這個做法主要在講解 context、context-free、context-aware 的概念,以及 ConfigureAwait 背後的原理。
首先定義 context-free 程式碼代表這段程式碼不必依賴 context 就能順利執行,context-aware 程式碼則是必須在特定的 context 才能順利執行, 所以你在隨意一個 context 運行 context-free 程式碼那麼不會發生什麼嚴重問題,但你在隨意一個 context 運行 context-aware 程式碼可能會發生嚴重錯誤。
會討論這個的原因是在前幾篇的做法中有提到,非同步方法有中斷與恢復的功能,那麼這個中斷會捕捉當時執行的 context 以後在恢復的時候理論上就可以避免 context switch 的發生避免影響性能,但實際上有些時候 context switch 反而會比不執行 context switch 性能來要好。
所以就有了 ConfigureAwait() 這個方法,它的用途是通知非同步方法不用在捕捉的 context 執行剩餘的程式碼, 設定為 false 就能讓剩餘的程式碼在新的 context 中執行,當然要確定剩餘的程式碼是 context-free 程式碼。
例如下面這段程式碼在非同步方法的後面加上 ConfigureAwait(false)
,通知剩餘的程式碼不用在捕捉的 context 執行剩餘的程式碼。
public static async Task<XElement> ReadPacket(string Url)
{
var result = await DownloadAsync(Url).ConfigureAwait(false);
return XElement.Parse(result);
}
接下來看看這段方法,你可能會認為 DownloadAsync
有設定 ConfigureAwait(false)
那麼之後的程式碼就不會強迫運行在捕捉的 context 中,
但實際上如果 DownloadAsync
執行的速度過快會讓整段程式碼以同步的方式運行,那其實就不會產生 context switch 也就是會一直運行在原始的 context 中,
要避免這個問題最好是把所有非同步方法都加上 ConfigureAwait(false)
。
public static async Task<Config> ReadConfig(string Url)
{
var result = await DownloadAsync(Url).ConfigureAwait(false);
var items = XElement.Parse(result);
var userConfig = from node in items.Descendants()
where node.Name == "Config"
select node.Value;
var configUrl = userConfig.SingleOrDefault();
if (configUrl != null)
{
result = await DownloadAsync(configUrl).ConfigureAwait(false);
var config = await ParseConfig(result).ConfigureAwait(false);
return config;
}
else
return new Config();
}
這個做法的困難點就是知道自己寫的程式碼是不是 context-aware 的,因為只有 context-aware 的程式碼才有必要運行在捕捉的 context 中,
另外一但離開捕捉的 context 那就沒辦法再回去了,通常只有更新 UI 的程式碼是 context-aware 的,應該把其它的程式碼都設定 ConfigureAwait(false)
。
像這段程式碼就是混合了 UI 更新的程式碼,就應該把 context-free 設定 ConfigureAwait(false)
,UI 更新的程式碼保持預設。
private async void OnCommand(object sender, RoutedEventArgs e)
{
var viewModel = (DataContext as SampleViewModel);
try
{
Config config = await ReadConfigAsync(viewModel);
await viewModel.Update(config);
}
catch (Exception ex) when (logMessage(viewModel, ex))
{
}
}
private async Task<Config> ReadConfigAsync(SampleViewModel viewModel)
{
var userInput = viewModel.webSite;
var result = await DownloadAsync(userInput).ConfigureAwait(false);
var items = XElement.Parse(result);
var userConfig = from node in items.Descendants()
where node.Name == "Config"
select node.Value;
var configUrl = userConfig.SingleOrDefault();
var config = default(Config);
if (configUrl != null)
{
result = await DownloadAsync(configUrl).ConfigureAwait(false);
config = await ParseConfig(result).ConfigureAwait(false);
}
else
config = new Config();
return config;
}
Summary
這個做法在新的 .NET Core 不會有影響,因為在已經沒有 SynchronizationContext
所以設定 ConfigureAwait(false)
並沒有影響,
但可能你的函式庫會與 ASP.NET 共用,所以一般還是建議都是加上 ConfigureAwait(false)
。