我正在努力了解这个简单程序中发生的事情。
在以下示例中,我有一个任务工厂,该工厂使用来自ParallelExtensionsExtras的LimitedConcurrencyLevelTaskScheduler,并将maxDegreeOfParallelism设置为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);
}
}
}
在此先感谢您的帮助
答案 0 :(得分:5)
await
by default captures the current context,并使用它来恢复async
方法。此上下文为SynchronizationContext.Current
,除非它为null
,在这种情况下为TaskScheduler.Current
。
在这种情况下,await
正在捕获用于执行LimitedConcurrencyLevelTaskScheduler
的{{1}}。因此,两次都启动DoWork
之后,这两个线程都被阻塞了(由于Task.Delay
)。 GetAwaiter().GetResult()
完成后,Task.Delay
将await
方法的其余部分安排到其上下文中。但是,上下文将不会运行它,因为它已经有2个线程。
因此,您最终会在上下文中阻塞线程,直到它们的HttpClientGetAsync
方法完成为止,但是async
方法无法完成,直到上下文中有可用线程为止;因此陷入僵局。与standard "don't block on async code" style of deadlock非常相似,只是使用 n 个线程而不是一个。
说明:
问题似乎是Task.Delay(2000)从未完成。
async
已完成,但是Task.Delay
无法继续执行await
方法。
如果将maxDegreeOfParallelism设置为3(或更大),则完成。但是在maxDegreeOfParallelism = 2(或更小)的情况下,我的猜测是没有可用的线程来完成任务。为什么会这样?
有很多可用的线程。但是async
一次只能在其上下文中运行2个线程。
它似乎与async / await有关,因为如果我删除它并在DoWork中简单执行Task.Delay(2000).GetAwaiter()。GetResult(),它就可以正常工作。
是; LimitedConcurrencyTaskScheduler
正在捕获上下文。 await
不会在内部捕获上下文,因此无需输入Task.Delay
就可以完成。
解决方案:
任务调度程序通常不能很好地与异步代码一起使用。这是因为任务计划程序是为并行任务而不是异步任务而设计的。因此,它们仅在代码运行(或被阻止)时适用。在这种情况下,LimitedConcurrencyTaskScheduler
仅“计数”正在运行的代码。如果您有一种正在执行LimitedConcurrencyLevelTaskScheduler
的方法,则不会根据该并发限制来“计数”。
因此,您的代码最终陷入具有同步异步模式的情况,这可能是因为有人试图避免await
在有限的并发任务调度程序中无法按预期工作的问题。然后,这种基于异步的反模式导致了死锁问题。
现在,您可以通过在各处使用await
来增加更多的黑客攻击,并继续阻塞异步代码,或您可以更好地对其进行修复。
更合适的解决方法是进行异步限制。完全扔出ConfigureAwait(false)
;并发限制任务调度程序仅适用于同步代码,并且您的代码是异步的。您可以使用LimitedConcurrencyLevelTaskScheduler
进行异步限制,如下所示:
SemaphoreSlim
答案 1 :(得分:0)
我认为您遇到同步死锁。您正在等待线程完成,而该线程正在等待线程完成。永远不会发生。如果您使DoWork方法异步,那么您可以等待HttpClientGetAsync()调用,从而避免死锁。
using MassTransit.Util;
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 async Task DoWork(int id)
{
Console.WriteLine($"Starting Work {id}");
await HttpClientGetAsync();
Console.WriteLine($"Finished Work {id}");
}
async Task HttpClientGetAsync()
{
await Task.Delay(2000);
}
}
}
https://medium.com/rubrikkgroup/understanding-async-avoiding-deadlocks-e41f8f2c6f5d
TLDR永远不会调用.result,我确定是.GetResult();。在做