带有Task.Result的线程池死锁

Bar*_*sch 6 c# multithreading deadlock task threadpool

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

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

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

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

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

最好的解决方案是将代码更改为一路异步工作,但这不是当前的选择。同样,删除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 "";
    }
}
Run Code Online (Sandbox Code Playgroud)

Pav*_*rov 0

您可以安全地使用简写形式 ( Foo().Result),而不是Task.Run(() => Foo()).Result禁用aspnet:UseTaskFriendlySynchronizationContext设置:

<appSettings>
   <add key="aspnet:UseTaskFriendlySynchronizationContext" value="false" />
</appSettings>
Run Code Online (Sandbox Code Playgroud)

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

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

您还可以使用<httpRuntime><processModel>配置最小空闲线程池大小:

<system.web>
  <processModel autoConfig="false" maxWorkerThreads="..." maxIoThreads="..." />
  <httpRuntime minFreeThreads="..." />
</system.web>
Run Code Online (Sandbox Code Playgroud)

请注意,默认值为:

每个 CPU 的 maxWorkerThreads = 100

每个 CPU 的 maxIoThreads = 100

每个 CPU 的最小空闲线程数 = 88