遇到了与异步有关的有趣的 livelock 情况。
考虑以下代码导致 livelock 并执行1分钟,即使有用的有效负载几乎不需要运行。执行时间大约为1分钟的原因是我们实际上将达到线程池增长限制(大约每秒1个线程),因此300次迭代将使其运行大约5分钟。
这是不琐碎的死锁,我们在SyncronizationContext
的环境中同步等待异步操作,只允许在单个线程上调度作业(例如WPF,WebAPI等)。下面的代码再现了Console Application上的一个问题,其中没有明确的SynchronizationContext
集,并且正在线程池上安排任务。
我知道"解决方案"这个问题是" asynchrony all the way"。实际上,我们可能不知道内部的某个地方 SyncMethod
的开发人员通过以阻塞的方式等待它来解除这些问题来抑制异步(即使他可能会执行trick }替换SynchronizationContext
使其至少不是死锁。
当"一直异步时,你有什么建议来处理这样的问题"不是一个选择?是否有其他东西而不是显而易见的"不会一次产生如此多的任务"?
void Main()
{
List<Task> tasks = new List<Task>();
for (int i = 0; i < 60; i++)
tasks.Add(Task.Run(() => SyncMethod()));
bool exit = false;
Task.WhenAll(tasks.ToArray()).ContinueWith(t => exit = true);
while (!exit)
{
Print($"Thread count: {Process.GetCurrentProcess().Threads.Count}");
Thread.Sleep(1000);
}
}
void SyncMethod()
{
SomethingAsync().Wait();
}
async Task SomethingAsync()
{
await Task.Delay(1);
await Task.Delay(1); // extra puzzle -- why commenting one of these Delay will partially resolve the issue?
Print("async done");
}
void Print(object obj)
{
$"[{Thread.CurrentThread.ManagedThreadId}] {DateTime.Now} - {obj}".Dump();
}
这是一个输出。注意所有异步延续几乎持续了一分钟,然后突然继续执行。
[12] 30.01.2018 23:34:36 - Thread count: 18 [12] 30.01.2018 23:34:37 - Thread count: 32 [12] 30.01.2018 23:34:38 - Thread count: 33 -- THREAD POOL STARTS TO GROW ... [12] 30.01.2018 23:35:18 - Thread count: 70 [12] 30.01.2018 23:35:19 - Thread count: 71 [12] 30.01.2018 23:35:20 - Thread count: 72 -- UNTIL ALL SCHEDULED TASKS CAN FIT [8] 30.01.2018 23:35:20 - async done -- ALMOST A MINUTE AFTER START [8] 30.01.2018 23:35:20 - async done -- THE CONTINUATIONS START GO THROUGH ... [61] 30.01.2018 23:35:20 - async done [10] 30.01.2018 23:35:20 - async done
答案 0 :(得分:0)
回答原始问题:
您对“异步”时如何处理此类问题有何建议? 一路走来”是没有选择的吗? 明显的“不一次产生这么多任务”吗?
绝对不是针对根本原因的解决方案,而是定量补救措施-我们可以使用SetMinThreads
来调整线程池,以增加将被创建的线程数量而不会造成延迟(因此这种方式比我的设置中每秒设置1个线程池线程的常规“注入速率”更快。它在给定设置中的工作方式很简单。基本上,我们浪费线程池线程,直到线程池变得足够大以开始执行继续。如果我们从足够大的池开始,则基本上是在消除我们受人为的“注入率”约束的时间段,该时间段试图将线程数量保持在较低水平(这很有意义,因为线程池旨在运行CPU绑定的任务而不是被阻止等待异步操作。
我还应该留下警告说明。
默认情况下,最小线程数设置为 系统上的处理器。您可以使用SetMinThreads方法来 增加最小线程数。但是,不必要 增加这些值可能会导致性能问题。如果太多 任务在同一时间启动,它们似乎都变慢了。在 在大多数情况下,线程池使用自己的算法会更好地执行 用于分配线程。将最小值减少到小于数量 的处理器也会损害性能。
还有一个有趣的问题,Microsoft建议在某些情况下增加ASP.NET的“最小线程数”,以提高性能/可靠性。
有趣的是,问题中描述的问题并非纯粹是虚构的。是真的。它发生在众所周知且广为接受的软件中。经验示例-Identity Server 3。
https://github.com/IdentityServer/IdentityServer3.EntityFramework/issues/101
具有此警告的实现(我们不得不将其重写以解决生产场景中的问题):
另一篇文章详细解释了这个问题。
关于单个Task.Delay
的异常行为,其中每个新注入的线程池线程都完成了一些异步调用。它似乎是由连续执行内联以及实现Task.Delay
和Timer
的方式引起的。看到此调用堆栈,它表明在处理线程池队列之前,新创建的线程池线程正在创建.NET计时器时对其进行了其他处理(请参阅System.Threading.TimerQueue.AppDomainTimerCallback
)。
at AsynchronySamples.StrangeTimer.Program.d__2.MoveNext() at System.Runtime.CompilerServices.AsyncMethodBuilderCore.MoveNextRunner.InvokeMoveNext(Object stateMachine) at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx) at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx) at System.Runtime.CompilerServices.AsyncMethodBuilderCore.MoveNextRunner.Run() at System.Runtime.CompilerServices.AsyncMethodBuilderCore.c__DisplayClass4_0.b__0() at System.Runtime.CompilerServices.AsyncMethodBuilderCore.ContinuationWrapper.Invoke() at System.Runtime.CompilerServices.TaskAwaiter.c__DisplayClass11_0.b__0() at System.Runtime.CompilerServices.AsyncMethodBuilderCore.ContinuationWrapper.Invoke() at System.Threading.Tasks.AwaitTaskContinuation.RunOrScheduleAction(Action action, Boolean allowInlining, Task& currentTask) at System.Threading.Tasks.Task.FinishContinuations() at System.Threading.Tasks.Task.FinishStageThree() at System.Threading.Tasks.Task`1.TrySetResult(TResult result) at System.Threading.Tasks.Task.DelayPromise.Complete() at System.Threading.Tasks.Task.c.b__274_1(Object state) at System.Threading.TimerQueueTimer.CallCallbackInContext(Object state) at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx) at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx) at System.Threading.TimerQueueTimer.CallCallback() at System.Threading.TimerQueueTimer.Fire() at System.Threading.TimerQueue.FireNextTimers() at System.Threading.TimerQueue.AppDomainTimerCallback(Int32 id) [Native to Managed Transition] at kernel32.dll!74e86359() at kernel32.dll![Frames below may be incorrect and/or missing, no symbols loaded for kernel32.dll] at ntdll.dll!77057b74() at ntdll.dll!77057b44()