[多线程] C#:为什么应该在库代码中使用ConfigureAwait(false)

英文原文: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) 配置任务,以便等待之后的继续操作不必在调用者上下文中运行,从而避免任何可能的死锁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值