活锁和抑制异步

时间:2018-01-30 19:59:26

标签: c# asynchronous async-await deadlock livelock

遇到了与异步有关的有趣的 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

1 个答案:

答案 0 :(得分:0)

回答原始问题:

  

您对“异步”时如何处理此类问题有何建议?   一路走来”是没有选择的吗?   明显的“不一次产生这么多任务”吗?

绝对不是针对根本原因的解决方案,而是定量补救措施-我们可以使用SetMinThreads来调整线程池,以增加将被创建的线程数量而不会造成延迟(因此这种方式比我的设置中每秒设置1个线程池线程的常规“注入速率”更快。它在给定设置中的工作方式很简单。基本上,我们浪费线程池线程,直到线程池变得足够大以开始执行继续。如果我们从足够大的池开始,则基本上是在消除我们受人为的“注入率”约束的时间段,该时间段试图将线程数量保持在较低水平(这很有意义,因为线程池旨在运行CPU绑定的任务而不是被阻止等待异步操作。

我还应该留下警告说明

  

默认情况下,最小线程数设置为   系统上的处理器。您可以使用SetMinThreads方法来   增加最小线程数。但是,不必要   增加这些值可能会导致性能问题。如果太多   任务在同一时间启动,它们似乎都变慢了。在   在大多数情况下,线程池使用自己的算法会更好地执行   用于分配线程。将最小值减少到小于数量   的处理器也会损害性能。

https://docs.microsoft.com/en-us/dotnet/api/system.threading.threadpool.setminthreads?view=netframework-4.8

还有一个有趣的问题,Microsoft建议在某些情况下增加ASP.NET的“最小线程数”,以提高性能/可靠性。

https://support.microsoft.com/en-us/help/821268/contention-poor-performance-and-deadlocks-when-you-make-calls-to-web-s

有趣的是,问题中描述的问题并非纯粹是虚构的。是真的。它发生在众所周知且广为接受的软件中。经验示例-Identity Server 3。

https://github.com/IdentityServer/IdentityServer3.EntityFramework/issues/101

具有此警告的实现(我们不得不将其重写以解决生产场景中的问题):

https://github.com/IdentityServer/IdentityServer3.EntityFramework/blob/master/Source/Core.EntityFramework/Serialization/ClientConverter.cs

另一篇文章详细解释了这个问题。

https://blogs.msdn.microsoft.com/vancem/2018/10/16/diagnosing-net-core-threadpool-starvation-with-perfview-why-my-service-is-not-saturating-all-cores-or-seems-to-stall/

关于单个Task.Delay的异常行为,其中每个新注入的线程池线程都完成了一些异步调用。它似乎是由连续执行内联以及实现Task.DelayTimer的方式引起的。看到此调用堆栈,它表明在处理线程池队列之前,新创建的线程池线程正在创建.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()