在非同步工作中使用 ConfigureAwait ,讓非同步方法當作同步方法呼叫,卻不會造成執行緒封鎖的做法
根據微軟官方文件上的說明,ConfigureAwait 代表的意思是 : 設定用來等候這個 Task 的 awaiter,而傳遞的參數為 continueOnCapturedContext ,這是一個 Boolean 型別,若該參數值為 true 表示嘗試將接續封送處理回原始擷取的內容,否則為 false。所以,當想要使用同步呼叫方式來呼叫非同步作業的時候,理論上應該是可以解決掉因為當非同步作業處理結束之後,要回到 同步內容 SynchronizationContext 後,卻不會造成執行緒封鎖的問題,不過,需要使用呼叫這個方法的時候,使用 false 引數, ConfigureAwait(false)
現在,來實際體驗一下,在此建立一個 WPF 的專案,因為 WPF 專案內的 UI 執行緒內會有使用到 同步內容 SynchronizationContext,用來測試要如何使用 ConfigureAwait ,但是卻不會造成執行緒封鎖的問題,在這裡的範例專案原始碼,可以從 GitHub 中取得。
在這個 WPF 專案內,建立出三個按鈕,分別用來展示出如何正確使用 ConfigureAwait(false),但是卻不會造成執行緒封鎖的問題。
<Window x:Class="HowConfigureAwait.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:HowConfigureAwait"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="50"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Button
x:Name="btnWillBlock" Content="會封鎖" Click="BtnWillBlock_Click"
FontSize="20"/>
<Button
Grid.Column="1"
x:Name="btnWillNotBlock" Content="不會封鎖(非同步工作使用ConfigureAwait)" Click="BtnWillNotBlock_Click"
FontSize="20"/>
<Button
Grid.Column="0" Grid.Row="1"
x:Name="btnWillNotBlockByAsyncMethod" Content="會封鎖(非同步方法使用ConfigureAwait)" Click="BtnWillNotBlockByAsyncMethod_Click"
FontSize="20"/>
<TextBlock
Grid.Row="2" Grid.ColumnSpan="2"
x:Name="tbMessage" FontSize="20" Foreground="Red"/>
</Grid>
</Window>
沒有使用 ConfigureAwait(false),而直接把非同步方法當作同步方式來呼叫
首先,左上角的按鈕,名稱為:會封鎖,當按下這個按鈕,將會使用同步方式
Method1Async().Result
來呼叫設計的非同步方法 async Task<string> Method1Async()
。在 Method1Async 非同步方法內,當執行這個 new HttpClient().GetStringAsync(url);
敘述之後,將會得到一個非同步工作物件,接著執行 await task.ConfigureAwait(true)
這個敘述。所以,底下的程式碼將會執行到 string result = task.Result;
敘述之後將會造成執行緒封鎖,因為,當剛剛的 new HttpClient().GetStringAsync(url);
非同步工作執行完畢之後,會需要回到 UI 執行緒下的 同步內容 SynchronizationContext,可是,因為 UI 執行緒下因為執行了 task.Result
,所以,UI 執行緒就會就會進入到封鎖階段,等候非同步工作結束執行,非同步工作執行完畢之後,也想要回到 UI 執行緒,這樣就造成了打死結了。private async void BtnWillBlock_Click(object sender, RoutedEventArgs e)
{
string result = Method1Async().Result;
Debug.WriteLine(result);
}
async Task<string> Method1Async()
{
Console.WriteLine($"呼叫 Method1Async 之前,執行緒 ID {Thread.CurrentThread.ManagedThreadId}");
Task<string> task = new HttpClient().GetStringAsync(url);
await task.ConfigureAwait(true);
string result = task.Result;
Console.WriteLine($"呼叫 Method1Async 之後,執行緒 ID {Thread.CurrentThread.ManagedThreadId}");
return " @@恭喜你 - Method1Async({result}) 已經執行完畢了@@ ";
}
呼叫非同步工作且使用 ConfigureAwait(false),而直接把非同步方法當作同步方式來呼叫
現在,來看看右上角的按鈕,名稱為:不會封鎖(非同步工作使用ConfigureAwait),當按下這個按鈕之後,將不會造成整個應用程式進入凍結的狀態,也就是沒有打死結。在這個按鈕事件被呼叫之後,同樣的使用同步方式來呼叫非同步方法,這裡使用的敘述是
Method2Async().Result
。
在非同步方法
async Task<string> Method2Async()
內,與上一個按鈕中使用到的 async Task<string> Method1Async()
非同步方法十分類似,唯一的差異就是要等候非同步工作執行完畢的時候,使用了 await task.ConfigureAwait(false);
敘述,也就是指定 await 這個運算子關鍵字,不需要捕捉當前的 同步內容 SynchronizationContext 物件,所以,當非同步工作執行完成之後,也就不會回到 同步內容 SynchronizationContext 的執行緒下。在這樣的程式碼,當非同步方法內的敘述 string result = task.Result;
雖然這個敘述是使用同步的方式來等待非同步的執行結果,但是,當非同步工作完成之後,這個敘述將會得到此非同步工作的執行結果,也不會造成專案凍結與執行緒封鎖的問題。private void BtnWillNotBlock_Click(object sender, RoutedEventArgs e)
{
string result = Method2Async().Result;
Debug.WriteLine(result);
tbMessage.Text = result;
}
async Task<string> Method2Async()
{
Console.WriteLine($"呼叫 Method2Async 之前,執行緒 ID {Thread.CurrentThread.ManagedThreadId}");
Task<string> task = new HttpClient().GetStringAsync(url);
await task.ConfigureAwait(false);
string result = task.Result;
Console.WriteLine($"呼叫 Method2Async 之後,執行緒 ID {Thread.CurrentThread.ManagedThreadId}");
return $" @@恭喜你 - Method2Async({result}) 已經執行完畢了@@ ";
}
呼叫非同步方法且使用 ConfigureAwait(false),而直接把非同步方法當作同步方式來呼叫
接著,來看看左上角的按鈕,名稱為:會封鎖(非同步方法使用ConfigureAwait),當按下這個按鈕之後,將整個應用程式進入凍結的狀態,也就是打死結了。
來看看這個按鈕事件程式碼,當這個事件被呼叫之後,
Task<string> task = Method3Async()
會使用這個方法來取得 Method3Async 非同步方法的工作物件,緊接著使用 task.ConfigureAwait(false)
敘述,指定這個非同步方法不要捕捉當前的 同步內容 SynchronizationContext,當然,當執行完畢之後,也就不會透過 同步內容 SynchronizationContext 回到當時的 UI 執行緒下。而在非同步方法 Method3Async ,將會與第一個,左上角的按鈕,會封鎖 按鈕相同,使用一般的等候工作做法,通常都會使用這樣的敘述 string result = await new HttpClient().GetStringAsync(url)
是相同的。
不幸的是,這樣的設計方式卻會造成整個應用程式凍結、打死結的狀態,可是,為什麼會這樣的,在這裡不是有使用到
task.ConfigureAwait(false)
敘述嗎?但是還是會打死結,問題地底出在哪裡呢?
請在 Method3Async 方法內的
await task.ConfigureAwait(true);
敘述上設定一個除錯中斷點,執行這個專案,並且按下左下角的按鈕,現在這個程式執行到剛剛設定的中斷點上,接著,按下 F10 不進入函式,然後,就看到了悲慘狀況,整個應用程式凍結了;因此,在按鈕事件上,要呼叫非同步方法的時候,使用 task.ConfigureAwait(false)
敘述是沒有問題,因為整個專案不是凍結在按鈕事件的任何一個敘述,而是凍結在非同步方法內的非同步工作呼叫上 await task.ConfigureAwait(true)
(這個敘述與 await task
相同的執行結果) 這個敘述。更進一步的解釋那就是,當呼叫這個敘述的時候,當前的執行緒依舊還是 UI 執行緒,並且因為 這個敘述 await task.ConfigureAwait(true)
指定要捕捉 同步內容 SynchronizationContext 物件,並於非同步工作完成之後,使用這個 同步內容 SynchronizationContext 物件回到 UI 執行緒下繼續來執行,結果就是同樣一句話,當前 UI 執行緒正處於封鎖狀態,所以,整個專案就打死結了。private void BtnWillNotBlockByAsyncMethod_Click(object sender, RoutedEventArgs e)
{
Task<string> task = Method3Async();
task.ConfigureAwait(false);
string result = task.Result;
Debug.WriteLine(result);
}
async Task<string> Method3Async()
{
Console.WriteLine($"呼叫 Method1Async 之前,執行緒 ID {Thread.CurrentThread.ManagedThreadId}");
Task<string> task = new HttpClient().GetStringAsync(url);
await task.ConfigureAwait(true);
string result = task.Result;
Console.WriteLine($"呼叫 Method1Async 之後,執行緒 ID {Thread.CurrentThread.ManagedThreadId}");
return " @@恭喜你 - Method1Async({result}) 已經執行完畢了@@ ";
}
結論
透過上面的實際範例程式碼展示,想要使用 ConfigureAwait(false) ,指定不要捕捉 同步內容 SynchronizationContext 物件,避免造成執行緒封鎖 Thread Block,就請記得要在 非同步工作 後面加上 ConfigureAwait(false) 敘述,而不要在 非同步方法 後面加上 ConfigureAwait(false) 敘述。
看不懂這句話的話,請要再度細細看過上面的內容,若還是無法理解,這就需要多看些書或者上課來補充這方面的相關知識囉。