源文链接:C#: Why you should use ConfigureAwait(false) in your library code (翻译:woodylic)
自从.NET 4.5引入了async/await以后,编写异步代码变得相当容易。Async/await关键字使得异步代码看起来和同步代码非常类似,这大大提高了代码的可读性以及编程效率,这多亏了编译器处理了异步编程最困难的部分。
下面例子展示了如何通过简单的代码完成一个异步操作:访问一个特定url,并把response内容写入字符串:
1
2
3
4
5
6
7
8
|
public async Task<string> DoCurlAsync()
{
using (var httpClient = new HttpClient())
using (var httpResonse = await httpClient.GetAsync("https://www.bynder.com"))
{
return await httpResonse.Content.ReadAsStringAsync();
}
}
|
至此,我们就拥有了一个从bynder.com获取内容的异步调用。理想情况下,这个异步调用会按以下方式被调用:
1
|
var bynderContents = await DoCurlAsync();
|
但实际上,有些开发人员可能会按以下方式调用:
1
|
var bynderContents = DoCurlAsync().Result;
|
在这种情况下,curl将以同步方式运行,调用进程会被阻塞至curl结束为止。如果这段代码在Console应用里执行,大部分时候也能正常执行。
但是,如果代码在一个UI应用执行,例如在一个按钮点击事件中触发:
1
2
3
4
|
public void OnButtonClicked(object sender, RoutedEventArgs e)
{
var bynderContents = DoCurlAsync().Result;
}
|
应用将会因为死锁而僵死,类库的用户也会抱怨我们的类库导致了应用无响应。
为了解决这个问题,我们可以重写我们的异步调用:
1
2
3
4
5
6
7
8
|
public async Task<string> DoCurlAsync()
{
using (var httpClient = new HttpClient())
using (var httpResponse = await httpClient.GetAsync("https://www.bynder.com").ConfigureAwait(false))
{
return await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
}
}
|
实际上,只要为httpClient.GetAsync()加上ConfigureAwait(false)就可以解决问题了。
1
2
3
4
5
6
7
8
|
public async Task<string> DoCurlAsync()
{
using (var httpClient = new HttpClient())
using (var httpResonse = await httpClient.GetAsync("https://www.bynder.com").ConfigureAwait(false))
{
return await httpResonse.Content.ReadAsStringAsync();
}
}
|
总的来说,总是为你的类库的异步调用加上ConfigureAwait(false)是一种推荐的做法,这可以避免(调用方造成的)不必要的问题。
接下来,我们会分析为什么(不当的async调用)在UI应用(以及少数Console应用)会造成死锁,以及为什么ConfigureAwait(false)可以解决死锁问题。
首先我们要了解UI应用的工作原理:
1
2
3
4
5
6
|
while((bRet = GetMessage( &msg, NULL, 0, 0 )) != 0)
{
// No errors are handled, for simplicity purposes.
TranslateMessage(&msg);
DispatchMessage(&msg);
}
|
- UI线程默认都会有一个同步上下文(SynchronizationContext)。
在UI进程执行了异步调用后,await后的代码会回到UI进程的SynchronizationContext,这是默认的并且是期望的行为。
如果从一个非UI进程修改一个UI组件,会抛出System.InvalidOperationException。
1
2
3
4
5
|
public void OnButtonClicked(object sender, RoutedEventArgs e)
{
var bynderContents = await DoCurlAsync();
myTextBlock.Text = bynderContents;
}
|
回到上面例子,DoCurlAsync从概念上等同于如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
var currentContext = SynchronizationContext.Current;
var httpResponseTask = httpClient.GetAsync("https://www.bynder.com");
httpResponseTask.ContinueWith(delegate
{
if (currentContext == null)
{
return await httpResonse.Content.ReadAsStringAsync();
}
else
{
currentContext.Post(delegate {
await httpResonse.Content.ReadAsStringAsync();
}, null);
}
}, TaskScheduler.Current);
|
注意:这里的代码片段只是await实际编译出来的代码的简化版本,而且也没有使用using来处理资源的关闭和释放。
Post调用会向UI进程的消息泵发送一条待处理消息,所以为了让DoCurlAsync调用完成,必须在UI进程执行await httpResonse.Content.ReadAsStringAsync()。
但是,在下面这个例子中:
1
2
3
4
|
public void OnButtonClicked(object sender, RoutedEventArgs e)
{
var bynderContents = DoCurlAsync().Result;
}
|
UI进程无法执行await httpResonse.Content.ReadAsStringAsync(),因为它已经被DoCurlAsync().Result阻塞了。这就产生了一个死锁,DoCurlAsync将无法结束。ConfigureAwait(false)配置指定await后的操作无需回到原来的context,因此解决了可能的异常。
References:
Asynchronous Programming with async and await (C#)
Await, SynchronizationContext, and Console Apps
Parallel Programming with .NET