与Task.Result的Threadpool死锁

时间:2017-07-04 07:07:19

标签: c# multithreading task threadpool deadlock

我们拥有asp.net系统的大量遗产,我们已经开始使用我们无法更改的基础架构库中的一些异步方法。 系统在大多数地方不使用任务,但基础设施只暴露异步方法。

在代码中我们使用以下模式来使用异步方法:

Task.Run(() => Foo()).Result

我们使用Task.Run来防止死锁,如果代码中的某个地方有人没有使用ConfigureAwait(false),有很多地方有人可能错过了以前发生过。 我们使用Task.Result将其与现有的同步代码库集成。

经历了沉重的负载后我们注意到我们正在超时但是服务器没有做任何工作(低CPU),我们发现当有很多对服务器和线程池的调用时在线程到达线程的最大线程数,因为它阻塞线程直到任务完成但任务无法运行,因为没有可用于运行它的线程池线程。

最好的解决方案是将代码更改为始终工作异步,但现在不是一个选项。 同时删除Task.Run可能会有效,但由于没有足够的测试覆盖率来知道我们不会在未经测试的流中导致新的死锁,因此风险太大。

我试图实现一个新的任务调度程序,它不会使用线程池,而是使用一组不同的线程来运行Foo任务,但内部任务正在我不想替换的默认任务调度程序上执行

如果没有对代码库进行大的更改,可以解决这个问题的任何想法吗?

这是一个小样本应用程序,只使用10个线程而不是真正的限制来重现问题。在样本中永远不会调用Foo。

class Program
{
    static void Main(string[] args)
    {
        ThreadPool.SetMaxThreads(10, 10);
        ThreadPool.SetMinThreads(10, 10);

        for (int i = 0; i < 10; i++)
        {
            ThreadPool.QueueUserWorkItem(CallBack);
        }

        Console.ReadKey();
    }

    private static void CallBack(object state)
    {
        Thread.Sleep(1000);

        var result = Task.Run(() => Foo()).Result;
    }

    public static async Task<string> Foo()
    {
        await Task.Delay(100);
        return "";
    }
}

2 个答案:

答案 0 :(得分:3)

您已经很好地解释了您的问题。

当你使用Task.Run然后阻塞结果时,你正在使用1-2个线程,当真正使用异步时你想要使用0-1个线程。

如果您在代码中过于宽松地使用Task.Run那么您可能会有多层阻塞线程,这使得线程使用变得非常难看,并且您将会遇到如您所述,最大容量。

顺便说一句,忘记尝试在单元测试(或控制台应用程序)中找到非同步死锁,因为它需要非默认SynchronizationContext

最佳和正确的解决方案是让所有内容从上到下异步或同步,但鉴于您受到限制,我建议调查this wonderful library from the Microsoft vs team并查看JoinableTaskFactory.Run(...),这将在阻塞线程上运行continuation,并在将此模式嵌套在多个级别时播放良好。使用这种方法,您将更接近同步等效代码。

重申一下,这些技术是变通方法,如果您通过尊重现有代码证明这些变通办法是合理的,那么尊重它的最佳方法就是正确执行,并使其完全同步,或从上到下异步。

答案 1 :(得分:0)

如果您停用Foo().Result设置,则可以安全地使用简短格式(Task.Run(() => Foo()).Result)代替aspnet:UseTaskFriendlySynchronizationContext

<appSettings>
   <add key="aspnet:UseTaskFriendlySynchronizationContext" value="false" />
</appSettings>

禁用任务友好的同步上下文意味着在任何await运算符之后HttpContext.Current将为null - 但现在它在Task.Run中为null。

使用Foo().Result代替Task.Run(() => Foo()).Result将导致线程池使用量减少2倍,因此可以解决您的问题。

此外,您可以使用<httpRuntime><processModel>配置最小可用线程池大小:

<system.web>
  <processModel autoConfig="false" maxWorkerThreads="..." maxIoThreads="..." />
  <httpRuntime minFreeThreads="..." />
</system.web>

请注意,默认值为:

  

maxWorkerThreads =每个CPU 100个

     

maxIoThreads =每个CPU 100个

     

minFreeThreads =每个cpu