使用LimitedConcurrencyLevelTask​​Scheduler和aync / await锁定问题

Dou*_*ble 0 .net c# task async-await

我正在努力了解这个简单程序中正在发生的事情。

在下面的示例中,我有一个任务工厂,该工厂使用ParallelExtensionsExtras中的LimitedConcurrencyLevelTask​​Scheduler,并将maxDegreeOfParallelism设置为2。

然后,我启动2个任务,每个任务都调用一个异步方法(例如,一个异步Http请求),然后获取等待者和已完成任务的结果。

问题似乎是Task.Delay(2000)永远无法完成。如果我将maxDegreeOfParallelism设置为3(或更大),它将完成。但是在maxDegreeOfParallelism = 2(或更小)的情况下,我的猜测是没有可用的线程来完成任务。这是为什么?

它似乎与async / await有关,因为如果我删除它并简单地Task.Delay(2000).GetAwaiter().GetResult()DoWork它中运行就完美了。异步/等待是以某种方式使用父任务的任务计划程序,或者它是如何连接的?

using System;
using System.Linq;
using System.Threading.Tasks;
using System.Threading.Tasks.Schedulers;

namespace LimitedConcurrency
{
    class Program
    {
        static void Main(string[] args)
        {
            var test = new TaskSchedulerTest();
            test.Run();
        }
    }

    class TaskSchedulerTest
    {
        public void Run()
        {
            var scheduler = new LimitedConcurrencyLevelTaskScheduler(2);
            var taskFactory = new TaskFactory(scheduler);

            var tasks = Enumerable.Range(1, 2).Select(id => taskFactory.StartNew(() => DoWork(id)));
            Task.WaitAll(tasks.ToArray());
        }

        private void DoWork(int id)
        {
            Console.WriteLine($"Starting Work {id}");
            HttpClientGetAsync().GetAwaiter().GetResult();
            Console.WriteLine($"Finished Work {id}");
        }

        async Task HttpClientGetAsync()
        {
            await Task.Delay(2000);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

预先感谢您的任何帮助

Ste*_*ary 5

await默认情况下,捕获当前上下文并使用它来恢复该async方法。在这种情况下SynchronizationContext.Current,除非是这样,否则上下文nullTaskScheduler.Current

在这种情况下,await正在捕获LimitedConcurrencyLevelTaskScheduler用于执行DoWork。因此,在Task.Delay两次启动之后,这两个线程均被阻塞(由于GetAwaiter().GetResult())。当Task.Delay完成时,await调度的剩余HttpClientGetAsync方法它的上下文。但是,上下文将不会运行它,因为它已经有2个线程。

因此,您最终会在上下文中阻塞线程,直到它们的async方法完成为止,但是async直到上下文中有可用线程时,这些方法才能完成。因此陷入僵局。与标准的“不要在异步代码上阻止”死锁样式非常相似,只是使用n个线程而不是一个线程。

说明:

问题似乎是Task.Delay(2000)从未完成。

Task.Delay正在完成,但是await无法继续执行该async方法。

如果我将maxDegreeOfParallelism设置为3(或更大),它将完成。但是在maxDegreeOfParallelism = 2(或更小)的情况下,我的猜测是没有可用的线程来完成任务。这是为什么?

有很多可用的线程。但是,LimitedConcurrencyTaskScheduler唯一一次允许2个线程在其上下文中运行。

它似乎与async / await有关,因为如果我删除它并简单地在DoWork中执行Task.Delay(2000).GetAwaiter()。GetResult(),它就可以完美地工作。

是; 它的await被捕获的上下文。Task.Delay不会在内部捕获上下文,因此无需输入即可完成LimitedConcurrencyTaskScheduler

解:

通常,任务计划程序在异步代码上不能很好地工作。这是因为任务计划程序是为并行任务而不是异步任务而设计的。因此,它们仅在代码正在运行(或被阻止)时适用。在这种情况下,LimitedConcurrencyLevelTaskScheduler仅“计数”正在运行的代码。如果您使用的方法正在执行await,则不会根据该并发限制来“计数”。

因此,您的代码最终处于具有异步同步反模式的情况,这可能是因为有人试图避免await使用有限的并发任务调度程序无法按预期工作的问题。然后,此异步同步模式导致了死锁问题。

现在,您可以ConfigureAwait(false)任何地方使用更多代码,从而继续阻塞异步代码,或者可以对其进行更好地修复。

一个更合适的解决方法是进行异步限制。LimitedConcurrencyLevelTaskScheduler完全扔掉;并发限制任务调度程序仅适用于同步代码,并且您的代码是异步的。您可以使用进行异步限制SemaphoreSlim,如下所示:

class TaskSchedulerTest
{
  private readonly SemaphoreSlim _mutex = new SemaphoreSlim(2);

  public async Task RunAsync()
  {
    var tasks = Enumerable.Range(1, 2).Select(id => DoWorkAsync(id));
    await Task.WhenAll(tasks);
  }

  private async Task DoWorkAsync(int id)
  {
    await _mutex.WaitAsync();
    try
    {
      Console.WriteLine($"Starting Work {id}");
      await HttpClientGetAsync();
      Console.WriteLine($"Finished Work {id}");
    }
    finally
    {
      _mutex.Release();
    }
  }

  async Task HttpClientGetAsync()
  {
    await Task.Delay(2000);
  }
}
Run Code Online (Sandbox Code Playgroud)