Dou*_*ble 0 .net c# task async-await
我正在努力了解这个简单程序中正在发生的事情。
在下面的示例中,我有一个任务工厂,该工厂使用ParallelExtensionsExtras中的LimitedConcurrencyLevelTaskScheduler,并将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)
预先感谢您的任何帮助
await默认情况下,捕获当前上下文并使用它来恢复该async方法。在这种情况下SynchronizationContext.Current,除非是这样,否则上下文null是TaskScheduler.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)
| 归档时间: |
|
| 查看次数: |
89 次 |
| 最近记录: |