0

遇到了与异步有关的有趣活锁情况。

考虑下面的代码,它会导致活锁并执行 1 分钟,即使有用的有效负载几乎不需要运行。执行时间大约为 1 分钟的原因是我们实际上会达到线程池增长限制(大约每秒 1 个线程),因此 300 次迭代将使其运行大约 5 分钟。

不是简单的死锁,我们在一个环境中同步等待异步操作,SyncronizationContext只允许在单个线程上调度作业(例如 WPF、WebAPI 等)。下面的代码重现了控制台应用程序上的一个问题,其中没有明确SynchronizationContext的设置并且任务正在线程池上调度。

我知道这个问题的“解决方案”是“一路异步”。实际上,我们可能不知道开发者内心深处的某个地方SyncMethod通过以阻塞方式等待它释放此类问题来抑制异步(即使他可能会通过替换来使其至少不会死锁) 。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 - 线程数:18
[12] 30.01.2018 23:34:37 - 线程数:32
[12] 30.01.2018 23:34:38 - 线程数:33 -- 线程池开始增长
...
[12] 30.01.2018 23:35:18 - 线程数:70
[12] 30.01.2018 23:35:19 - 线程数:71
[12] 2018 年 1 月 30 日 23:35:20 - 线程数:72 - 直到所有计划任务都适合
[8] 30.01.2018 23:35:20 - 异步完成 - 开始后几乎一分钟
[8] 2018 年 1 月 30 日 23:35:20 - 异步完成 - 继续进行
...
[61] 30.01.2018 23:35:20 - 异步完成
[10] 30.01.2018 23:35:20 - 异步完成
4

1 回答 1

0

回答原来的问题:

当无法选择“一路异步”时,您对处理此类问题有何建议?除了明显的“不要一次产生这么多任务”之外,还有其他东西吗?

绝不是根本原因的解决方案,而是定量的补救措施- 我们可以通过增加将立即创建的线程数量来调整线程池SetMinThreads(这样比我的设置 1 中的常规“注入率”更快)线程池线程每秒)。它在给定设置中的工作方式很简单。基本上我们在浪费线程池线程,直到池增长到足以开始执行延续。如果我们从足够大的池开始,我们基本上消除了我们只是受人为“注入率”约束的时间段,它试图保持低线程数量(这是有道理的,因为线程池旨在运行受 CPU 限制的任务而不是被阻塞等待异步操作)。

我也应该留下警告信

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

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

还有一个有趣的问题,微软建议在某些情况下增加 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-或-似乎停滞不前/

至于单个的奇怪行为,Task.Delay其中每个新注入的线程池线程都完成了一些异步调用。它似乎是由继续执行内联的方式引起的,Task.Delay并被Timer执行。请参阅此调用堆栈,它表明新创建的线程池线程在创建时正在为 .NET 计时器执行一些额外的魔法,然后再处理线程池队列(请参阅 参考资料System.Threading.TimerQueue.AppDomainTimerCallback)。

   在 AsynchronySamples.StrangeTimer.Program.d__2.MoveNext()
   在 System.Runtime.CompilerServices.AsyncMethodBuilderCore.MoveNextRunner.InvokeMoveNext(对象状态机)
   在 System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext,ContextCallback 回调,对象状态,布尔值 preserveSyncCtx)
   在 System.Threading.ExecutionContext.Run(ExecutionContext executionContext,ContextCallback 回调,对象状态,布尔值 preserveSyncCtx)
   在 System.Runtime.CompilerServices.AsyncMethodBuilderCore.MoveNextRunner.Run()
   在 System.Runtime.CompilerServices.AsyncMethodBuilderCore.c__DisplayClass4_0.b__0()
   在 System.Runtime.CompilerServices.AsyncMethodBuilderCore.ContinuationWrapper.Invoke()
   在 System.Runtime.CompilerServices.TaskAwaiter.c__DisplayClass11_0.b__0()
   在 System.Runtime.CompilerServices.AsyncMethodBuilderCore.ContinuationWrapper.Invoke()
   在 System.Threading.Tasks.AwaitTaskContinuation.RunOrScheduleAction(Action 动作,布尔 allowInlining,Task& currentTask)
   在 System.Threading.Tasks.Task.FinishContinuations()
   在 System.Threading.Tasks.Task.FinishStageThree()
   在 System.Threading.Tasks.Task`1.TrySetResult(TResult 结果)
   在 System.Threading.Tasks.Task.DelayPromise.Complete()
   在 System.Threading.Tasks.Task.c.b__274_1(对象状态)
   在 System.Threading.TimerQueueTimer.CallCallbackInContext(对象状态)
   在 System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext,ContextCallback 回调,对象状态,布尔值 preserveSyncCtx)
   在 System.Threading.ExecutionContext.Run(ExecutionContext executionContext,ContextCallback 回调,对象状态,布尔值 preserveSyncCtx)
   在 System.Threading.TimerQueueTimer.CallCallback()
   在 System.Threading.TimerQueueTimer.Fire()
   在 System.Threading.TimerQueue.FireNextTimers()
   在 System.Threading.TimerQueue.AppDomainTimerCallback(Int32 id)
   [本机到托管转换]   
   在 kernel32.dll!74e86359()
   在 kernel32.dll![下面的帧可能不正确和/或丢失,没有为 kernel32.dll 加载符号]
   在 ntdll.dll!77057b74()
   在 ntdll.dll!77057b44()  

于 2019-11-17T19:47:29.220 回答