C#中async的死鎖分析和解決方案


死鎖示例

如果你開發一個簡單的Windows Form程序,點擊Button去使用async異步獲取一個數據,然后顯示在Label上,類似這樣的代碼

private void button1_Click(object sender, EventArgs e) { var task = GetContentAsync(); var content = task.Result; this.label1.Text = content; } public async Task<string> GetContentAsync() { var http = new HttpClient(); var result = await http.GetStringAsync("http://www.imzjy.com"); var first50 = result.Substring(0, 50); return first50; } 

當你點擊Button的時候會發現程序直接卡死了。

死鎖原因分析

C#中的async/await隱藏了很多的細節,一個簡單的await其實讓函數發生了一次重入,重入對於多線程代碼來說其實很正常。但是C#將這些藏了起來。你看上去像一個函數,其實被分成了兩段,而且執行這兩段代碼的線程還可能不一樣。

上面代碼真正的執行過程是這樣的:

private void button1_Click(object sender, EventArgs e) { //1. calling GetContentAsync var task = GetContentAsync(); Debug.WriteLine($"Continuation:{Environment.CurrentManagedThreadId}"); //4. .Result(or GetAwait().GetResult()) which waiting for GetContentAsync to complete. //OOPS: DEADLOCK!!! //REASON: task.Result waiting the http.GetStringAsync complete and return; //REASON: GetContentAsync wait button1_Click release the synchronization context; var content = task.Result; this.label1.Text = content; } public async Task<string> GetContentAsync() { var http = new HttpClient(); //2. automatic capture synchronization context :: auto capture caused the issue. //3. due to await applied, yield thread to caller(button1_Click) var result = await http.GetStringAsync("http://www.imzjy.com"); //WHY AUTO CAPTURE? capture the synchronization context makes following accessing UI control became possible. //textBox1.Text = first50; var first50 = result.Substring(0, 50); return first50; } 

對於GetContentAsync函數來說,在await之前其實是同步的代碼,當await之后,線程直接返回給button1_Clickawait時候發生了兩件事:

  • 在返回之前偷偷做了個動作,那就是將當前線程的同步上下文(SychronizationContext)給捕獲了。
  • 返回了一個未完成的任務,這里面抽象為Task

然后在第4步,當button1_Click中去獲取上面這個Task返回值的時候出現了死鎖,button1_ClickGetContentAsync相互等待:

  1. var content = task.Result; button1_Click等待任務await http.GetStringAsync("http://www.imzjy.com");完成
  2. await http.GetStringAsync("http://www.imzjy.com");等待當前線程(UI線程)的同步上下文SychronizationContext

由於上面兩個方法相互等待,所以產生了死鎖。

為什么自動捕獲當前線程同步上下文

GetContentAsync自動捕獲的是當前UI線程的同步上下文,通過偷偷的捕獲當前UI線程的同步上下文可以讓你在GetContentAsync方法中await之后可以更新UI控件。如果你在GetContentAsync中不需要更新UI控件,那么我們就不必捕獲同步上下文,那么也就不存在這個問題。

解決方案 1

修改GetContentAsync,讓http.GetStringAsync("http://www.imzjy.com”);自動捕獲上下文時候捕獲不到。破壞了上面的死鎖條件2。

private void button1_Click(object sender, EventArgs e) { var task = GetContentAsync(); var content = task.Result; this.label1.Text = content; } public async Task<string> GetContentAsync() { var syncContext = WindowsFormsSynchronizationContext.Current; //save SynchronizationContext WindowsFormsSynchronizationContext.SetSynchronizationContext(null); //set SynchronizationContext to null var http = new HttpClient(); var result = await http.GetStringAsync("http://www.imzjy.com"); WindowsFormsSynchronizationContext.SetSynchronizationContext(syncContext); //restore the SynchronizationContext var first50 = result.Substring(0, 50); return first50; } 

優點:

  1. 調用方代碼不需要改變

缺點:

  1. 調用者線程(UI線程)會在var content = task.Result;阻塞,直到GetContentAsync返回,導致界面在此期間無響應。
  2. 如果異步方法類似http.GetStringAsync("http://www.imzjy.com")需要更新界面(使用UI線程)會出現問題
  3. 改的代碼比較多3行。
  4. WindowsFormsSynchronizationContext.SetSynchronizationContext(null);可能有副作用。

解決方案 2

修改調用方式,將調用放到Thread pool中,這樣await http.GetStringAsync("http://www.imzjy.com");的auto capture就不會獲取到當前UI線程的SynchronizationContext,破壞了上面的死鎖條件2。

private void button1_Click(object sender, EventArgs e) { //put the GetContentAsync into thread pool, so that //http.GetStringAsync("http://www.imzjy.com"); //will capture the SynchronizationContext from thread pool's excection environemnt var task = Task<string>.Run(GetContentAsync); var content = task.Result; this.label1.Text = content; } public async Task<string> GetContentAsync() { var http = new HttpClient(); var result = await http.GetStringAsync("http://www.imzjy.com"); var first50 = result.Substring(0, 50); return first50; } 

優點:

  1. async方法不需要改變。

缺點:

  1. 調用者線程(UI線程)會在var content = task.Result;阻塞,直到GetContentAsync返回,導致界面在此期間無響應。
  2. 如果異步方法類似http.GetStringAsync("http://www.imzjy.com")需要更新界面(使用UI線程)會出現問題

解決方案 3

通過ConfigureAwait來改變自動捕獲SynchronizationContext行為,破壞了上面的死鎖條件2。

private void button1_Click(object sender, EventArgs e)
{
    var task = GetContentAsync();

    var content = task.Result;
    this.label1.Text = content;
}


public async Task<string> GetContentAsync()
{
    var http = new HttpClient();
    //tell await not to capture SynchronizationContext
    var result  = await http.GetStringAsync("http://www.imzjy.com”)
.ConfigureAwait(continueOnCapturedContext: false); var first50 = result.Substring(0, 50); return first50; }

優點:

  1. 調用方(caller)不需要改變
  2. 避免了此處無用的自動捕獲線程上下文。

缺點:

  1. 調用者線程(UI線程)會在var content = task.Result;阻塞,直到GetContentAsync返回,導致界面在此期間無響應。
  2. 如果異步方法類似http.GetStringAsync("http://www.imzjy.com")需要更新界面(使用UI線程)會出現問題

解決方案 4

把當前的事件處理函數也改成async的,這樣破壞了死鎖條件的1。button1_Click不在死等,所以也釋放了上下文。

private async void button1_Click(object sender, EventArgs e) { var task = GetContentAsync(); var content = await task; this.label1.Text = content; } public async Task<string> GetContentAsync() { var http = new HttpClient(); var result = await http.GetStringAsync("http://www.imzjy.com"); var first50 = result.Substring(0, 50); textBox1.Text = first50; return first50; } 

優點:

  1. async方法不需要改變。
  2. 避免了UI無響應的問題。
  3. GetContentAsyncawait之后可以更新UI界面。

缺點:

  1. button1_Click改為了異步,對原來的方法有侵入性,甚至會改變整個調用鏈的行為,我最討厭這點了。

適用

上面的死鎖通常會發生在下面兩個地方

  1. Windows Forms的UI線程中調用了異步的方法。
  2. ASP.NET的User Request Context執行環境,比如Controller中的方法。 代碼細節

經驗

異步方法實現者

  1. 分開提供同步和異步方法
  2. 只是自己做一些事,不需要bind到調用線程上的需要盡量.ConfigAwait(continueOnCapturedContext:false)

對於異步方法使用者

  1. 看看是否提供了同步方法
  2. 考慮是否有機會將自己的代碼轉為異步代碼
  3. 實在不行放到threadpool中去執行

async/await中的異常處理

如果加上異常處理,那么async/await會變得更加復雜,因為異步方法在異步執行,所以可以放到不同的線程中,那么如果出現了異常會怎么樣?簡單來說:

  1. 異步代碼中的異常如果存在TaskTask<T>被attach到了Task對象上
  2. 但是async void例外,由於沒有Task對象可以attach,所以attach到了SynchronizationContext中活躍的線程中了。
  3. 異步方法調用(調用鏈)中的異常,會被Aggregate,然后生成一個AggregateException。你可以使用aggExp.Flatten()方法來方便查看這個調用鏈中所有異常--如果有多個的話。

完整代碼示例

AsyncLockAndFixes

 

首發:https://www.imzjy.com/blog/dotnet-async-locks-and-solutions


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM