英文原文:https://medium.com/bynder-tech/c-why-you-should-use-configureawait-false-in-your-library-code-d7837dce3d7f
在.NET4.5中引入async/await之后,编写异步代码变得相对容易。 Async/await 关键字提高了代码的可读性和程序员的工作效率,因为代码类似于同步代码,并且易于理解,这要归功于编译器处理了异步编程中最困难的部分。
让我们创建一个示例,看看编写异步代码来卷曲特定 URL 并将其内容作为字符串返回是多么容易。
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 的内容时不会阻塞调用线程。
在理想的世界中,人们总是会通过以下方式使用我们的库:
var bynderContents = await DoCurlAsync();
然而,编程世界远非理想,有些人可能会通过以下方式使用它:
var bynderContents = DoCurlAsync().Result;
这样,curl 就会以同步方式完成,阻塞调用线程直到curl 完成。 如果这是在控制台应用程序中执行,大多数时候我们的代码将按预期运行(不一定总是)。
但是,如果该代码在 UI 应用程序中运行,例如单击按钮时,如下例所示:
public void OnButtonClicked(object sender, RoutedEventArgs e)
{
var bynderContents = DoCurlAsync().Result;
}
然后应用程序将冻结并停止工作,我们就陷入了僵局。 当然,我们库的用户会抱怨,因为它使应用程序无响应。
为了解决这个问题并使我们的库在这种情况下工作,我们必须重写我们的函数,如下所示:
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);
}
}
其实只要加上第一个ConfigureAwait(false)就足以解决问题了。
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) 是一种很好的做法,可以防止出现不必要的问题。
现在,我们将分析为什么 UI 应用程序中会发生死锁(而不是大多数控制台应用程序中),以及为什么ConfigureAwait(false) 可以解决这个问题。
首先我们需要了解大多数 UI 应用程序是如何工作的:
- 有一个线程负责 UI:UI 线程。 只有从该线程调用,UI 才能更新,因此如果该线程被阻塞,应用程序将变得无响应。
- UI 线程有一个消息队列来接收要执行的通知/操作。 在 Win32 中,这会转化为如下内容:
while( (bRet = GetMessage( &msg, NULL, 0, 0 )) != 0)
{
// No errors are handled, for simplicity purposes.
TranslateMessage(&msg);
DispatchMessage(&msg);
}
- UI线程默认有一个SynchronizationContext。
如果存在 SynchronizationContext(即我们位于 UI 线程中),await 之后的代码将在原始线程上下文中运行。 这是默认和预期的行为。
如果我们要从 UI 线程以外的线程修改 UI 组件,则会抛出“System.InvalidOperationException”,如下例所示:
public void OnButtonClicked(object sender, RoutedEventArgs e)
{
var bynderContents = await DoCurlAsync();
myTextBlock.Text = bynderContents;
}
回到我们的工作示例,我们的异步 DoCurlAsync 调用在概念上等同于:
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 httpResponse.Content.ReadAsStringAsync()。
但是,在以下场景中:
public void OnButtonClicked(object sender, RoutedEventArgs e)
{
var bynderContents = DoCurlAsync().Result;
}
UI 线程无法处理该指令,因为它已被阻止。 我们陷入了僵局,因为 DoCurlAsync 永远不会完成。 ConfigureAwait(false) 配置任务,以便等待之后的继续操作不必在调用者上下文中运行,从而避免任何可能的死锁。