簡體   English   中英

與Task.Result的Threadpool死鎖

[英]Threadpool deadlock with Task.Result

我們擁有asp.net系統的大量遺產,我們已經開始使用我們無法改變的基礎架構庫中的一些異步方法。 系統在大多數地方不使用任務,但基礎結構僅公開異步方法。

在代碼中,我們使用以下模式來使用異步方法:

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

我們使用Task.Run來防止死鎖,如果代碼中的某個地方有人沒有使用ConfigureAwait(false),有很多地方有人可能錯過了它以前發生過。 我們使用Task.Result將其與現有的同步代碼庫集成。

經歷了沉重的負載后我們注意到我們正在超時,但服務器沒有做任何工作(低CPU),我們發現當有很多調用服務器並且線程池達到最大值時線程在Task.Result中達到死鎖的線程數,因為它阻塞線程直到任務完成但任務無法運行,因為沒有線程池線程可用於運行它。

最好的解決方案是將代碼更改為始終工作異步,但現在不是一個選項。 同時刪除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 "";
    }
}

您已經很好地解釋了您的問題。

當您使用Task.Run然后阻塞結果時,您使用1-2個線程,當真正使用異步時您想要使用0-1個線程。

如果您通過代碼過度使用Task.Run那么您可能會有多層阻塞線程,這使得線程使用變得非常難看,並且您將達到您所描述的最大容量。

順便說一句,忘記嘗試在單元測試(或控制台應用程序)中找到異步死鎖,因為它需要作為非默認的SynchronizationContext

最好和最正確的解決方案是使所有內容從上到下異步或同步,但鑒於您受到限制,我建議從Microsoft vs團隊調查這個精彩的庫並查看JoinableTaskFactory.Run(...) ,這將在阻塞線程上運行continuation,並且當您將此模式嵌套在多個級別時可以很好地運行。 使用這種方法,您將更接近同步等效代碼。

重申一下,這些技術是變通方法,如果您通過尊重現有代碼證明這些變通方法是正確的,那么尊重它的最佳方法就是正確執行,並使其完全同步或從上到下異步。

您可以安全地使用縮寫形式( Foo().Result )而不是Task.Run(() => Foo()).Result如果禁用aspnet:UseTaskFriendlySynchronizationContext 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 88個

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM