簡體   English   中英

如果使用 ConfigureAwait(false) 調用 Task.Result 會導致死鎖嗎?

[英]Will calling Task.Result cause a deadlock if ConfigureAwait(false) is used?

我找到了這個方法:

public override void OnActionExecuting(HttpActionContext actionContext)
{
    var body = Task.Run(async()
    => await actionContext.Request.Content.ReadAsStringAsync()
        .ConfigureAwait(false)).Result;
    //rest of code omitted for brevity.
}

我正在嘗試解決兩件事:

1.這段代碼會導致死鎖嗎?

2.既然方法不能標記為async Task ,是不是應該這樣寫?

var body = actionContext.Request.Content.ReadAsStringAsync().ConfigureAwait(false).GetAwaiter().GetResult();

ConfigureAwait(false)用於跳過任何同步上下文和 go 直接到線程池繼續工作 因此,我認為您正在處理同步上下文出現問題(單線程上下文)的情況,例如 Windows Forms。

問題本質上是,如果您使用.Result.Wait()鎖定同步上下文的唯一線程,那么就沒有線程可以完成任何后續工作,從而有效地凍結了您的應用程序。 通過確保線程池線程將使用ConfigureAwait(false)完成工作,然后,至少在我看來,一切都應該像您懷疑的那樣好。 我認為這是您的一個非常好的發現。

然而,正如在給定的評論中已經指出的那樣,這不是您顯示的特定代碼的情況。 對於初學者,您明確地開始了一個新線程。 僅此一項就可以使您擺脫擔心的情況。

澄清:任務不是線程

如評論中所述,我會澄清。 我在最后一段中通過說“開始一個新線程”來簡化。 為了更開明,我會詳細說明。

每當您使用Task.Run()時,新創建的任務都會 go 位於線程池任務隊列中。 有全局隊列,每個線程池線程有一個隊列。 這是有趣的部分:

  • 如果執行Task.Run()語句的線程是線程池線程,則新任務將放在該線程任務隊列的最后。
  • 如果執行Task.Run()語句的線程不是線程池線程,則新任務將放在全局任務隊列的最后。

此時,由於我們假設我們沒有等待,而是讓執行線程休眠直到任務完成,使用.Result.Wait() ,我們進入以下情況:

  • A :執行線程是一個線程池線程。 這個線程現在被阻塞,因此 100% 無法從它自己的任務隊列中取出任何東西。
  • B :執行線程不是線程池線程。 現在線程被阻塞,任務進入全局隊列。

如果爬山算法當前正在創建新線程,或者尚未達到最小線程數,則會創建新的線程池線程。 每個新線程將從全局任務隊列中出列。 如果那里沒有任務,它們將繼續從各個線程的任務隊列中竊取工作。

這意味着,通過.Result 'ing 或.Wait() 'ing 從一個線程已經把我們置於微妙的 position 中,不得不等待另一個線程池線程來拯救我們。 這是……脆? 如果全局隊列接收任務的速度快於新線程添加到線程池的速度怎么辦? 然后我們有一個凍結的線程,就好像它死鎖了一樣。

這就是為什么通常不鼓勵使用.Result.Wait()的原因。

無論如何,我簡化了我所做的方式,因為在任何一種情況下,您都必須等待不同的線程來拯救您。 通過編寫這樣的代碼,您可以保證當前線程永遠無法自救。 因此,“新線程”。

這段代碼會導致死鎖嗎?

 string body = Task.Run(async() => { return await actionContext.Request.Content.ReadAsStringAsync().ConfigureAwait(false); }).Result;

ReadAsStringAsync任務是在ThreadPool上調用的,並且ThreadPool沒有同步上下文 因此await將找不到要捕獲的同步上下文。 因此.ConfigureAwait(false)無效。 您可以省略它,或將continueOnCapturedContext設置為true ,結果將是相同的:沒有死鎖。

應該這樣寫嗎?

 string body = actionContext.Request.Content.ReadAsStringAsync().ConfigureAwait(false).GetAwaiter().GetResult();

此代碼也不會導致死鎖。 原因是因為ReadAsStringAsync方法沒有在內部捕獲同步上下文。 其內部實現中的任何await都配置有.ConfigureAwait(false) 這是你無法控制的事情。 這就是這個方法的實現方式。 您在.GetAwaiter().GetResult()之前插入的 .ConfigureAwait .ConfigureAwait(false)無效。 當存在同步上下文時, ConfigureAwait方法會影響await的行為。 在您的情況下沒有await ,因此是否存在同步上下文無關緊要。 .GetAwaiter().GetResult()不是await 因此,將continueOnCapturedContext設置為falsetrue或者完全省略ConfigureAwait都沒有關系。 結果是一樣的:沒有死鎖。

由於您的代碼是 ASP.NET 應用程序的一部分,因此不建議使用這兩種方法。 ReadAsStringAsync操作正在進行時,兩者都不必要地阻塞了ThreadPool線程。


更新:正如 Alexei Levenkov 在下面的評論中指出的那樣,在 ASP.NET 應用程序中使用 .Wait( .Wait().Result.GetAwaiter().GetResult() 阻塞異步代碼會帶來整個站點完全死鎖的風險。 如果ThreadPool可用性已用盡,就會發生這種情況。 ThreadPool可以創建相當多的線程,在我運行 .NET 6 的 Windows 10 / 64 位機器中恰好有 32,767 個線程(正如我通過調用ThreadPool.GetMaxThreads方法發現的),所以耗盡它並不是一件容易的事。 但是,如果您的站點很忙並且您經常阻塞異步代碼,那也不是不可能。 如果發生這種情況,那么正在完成的異步操作將找不到可用的線程來安排它們的延續。 .Wait()正在阻塞ManualResetEventSlim上的當前線程,等待來自另一個線程的信號。 信號永遠不會到來,因此.Wait()將永遠阻塞。 然后整個站點將完全停止。 在進程被回收之前,不會再有請求被處理。 這是 Stephen Toub 的一篇相關文章: 我應該為異步方法公開同步包裝器嗎?

暫無
暫無

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

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