C# 不要阻塞异步代码,即异步代码死锁的最佳解决方案

这是一个在论坛和 Stack Overflow 上反复提出的问题。我认为这是异步新手在学习了基础知识后最常问的问题。

用户界面示例

勇哥编写了下面的例子。单击按钮将启动 REST 调用并在文本框中显示结果(此示例适用于 Windows 窗体,但相同的原则适用于任何UI 应用程序)。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            var jsonTask = GetJsonAsync(new Uri("http://www.baidu.com"));
            textBox1.Text = jsonTask.GetAwaiter().GetResult().ToString();
        }
        public static async Task<int> GetJsonAsync(Uri uri)
        {
            using (var client = new HttpClient())
            {
                var jsonString = await client.GetStringAsync(uri); 
                return 0;
            }
        }

    }
}

image.png

代码的功能是按下按钮,调用异步方法GetJsonAsync从网站取到文本,显示到textbox中去。

这里勇哥在异步方法中直接返回了一个0,没有用到获取到的文本内容。

这段代码运行后会死锁。



导致死锁的原因

情况是这样的从我的介绍文章中记住,在等待 Task 之后,当方法继续时,它将在 context 中继续。

在第一种情况下,此上下文是一个 UI 上下文(适用于除控制台应用程序之外的任何UI)。在第二种情况下,此上下文是 ASP.NET 请求上下文。

另外一个很重要的一点:ASP.NET请求上下文不依赖于特定的线程(如UI上下文),但它确实只允许一个线程在同一时间。这个有趣的方面在 AFAIK 的任何地方都没有正式记录,但在我关于 SynchronizationContext 的 MSDN 文章中提到

所以这就是发生的事情,从顶级方法(用于 UI 的 Button1_Click / 用于 ASP.NET 的 MyController.Get)开始:

        1. 顶级方法调用GetJsonAsync(在ui的上下文中)

        2. GetJsonAsync通过调用client.GetStringAsync(仍在ui上下文中)启动rest请求

        3. client.GetStringAsync返回一个未完成的task,表示rest请求未完成。

        4. GetJsonAsync等待client.GetStringAsync返回的Task。上下文被捕获,

           稍后将用于继续运行GetJsonAsync方法。

           GetJsonAsync返回一个未完成的Task,说明GetJsonAsync方法未完成。

        5. 顶层方法同步阻塞GetJsonAsync返回的Task。这会阻塞上下文线程。

        6. 最终,rest请求完成。这完成了由client.GetStringAsync返回的任务。

        7. GetJsonAsync的延续现在已准备好运行,它等待上下文可用,以便它可以在上下文中执行。

        8. 僵局。顶层方法正在阻塞上下文线程,等待GetJsonAsync完成,

           而GetJsonAsync正在等待上下文空闲以便它可以完成。


对于 UI 示例,“上下文”是 UI 上下文;对于 ASP.NET 示例,“上下文”是 ASP.NET 请求上下文。任何一种“上下文”都可能导致这种类型的死锁。


防止死锁

有两个最佳实践(都在我的介绍文章介绍可以避免这种情况:

  1. 在您的“库”异步方法中,尽可能使用 ConfigureAwait(false)。

  2. 不要阻塞任务;一直使用异步。

考虑第一个最佳实践。新的“GetJsonAsync”方法如下所示:

注意仅仅是添加了方法.ConfigureAwait(false); 

   public static async Task<int> GetJsonAsync(Uri uri)
        {
            using (var client = new HttpClient())
            {
                var jsonString = await client.GetStringAsync(uri).ConfigureAwait(false); 
                return 0;
            }
        }

.ConfigureAwait(false) 改变GetJsonAsync的延续行为,因此,它并没有上下文恢复。相反,GetJsonAsync 将在线程池线程上恢复。这使 GetJsonAsync 能够完成它返回的任务,而无需重新输入上下文。同时,顶级方法确实需要上下文,因此它们不能使用ConfigureAwait(false).


使用ConfigureAwait(false)来避免死锁是一种危险的做法。
你将不得不使用ConfigureAwait(false)的每一个 await在所有被拦截的代码调用的方法,传递闭包,包括所有的第三方和第二方的代码。
使用ConfigureAwait(false)以避免死锁充其量只是一个黑客)。
正如这篇文章的标题所指出的,更好的解决方案是“不要阻塞异步代码”。


第二种防死锁的办法是一路异步,如下:

这改变了顶级方法的阻塞行为,因此上下文永远不会被真正阻塞;所有“等待”都是“异步等待”。

        private async void button1_Click(object sender, EventArgs e)
        {
            //var jsonTask = GetJsonAsync(new Uri("http://www.baidu.com"));
            //textBox1.Text = jsonTask.GetAwaiter().GetResult().ToString();

            var jsonTask = await GetJsonAsync(new Uri("http://www.baidu.com"));
            textBox1.Text = jsonTask.ToString();
        }


这种死锁总是同步和异步代码混合的结果。通常这是因为人们只是在一小段代码中尝试异步,而在其他地方使用同步代码。不幸的是,部分异步代码比使所有内容都异步要复杂和棘手得多。


注意:最好同时应用这两种最佳实践。任何一种都可以防止死锁,但两者都必须应用以实现最大的性能和响应能力。


本文出自勇哥的网站《少有人走的路》wwww.skcircle.com,转载请注明出处!讨论可扫码加群:

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

会员中心
搜索
«    2024年4月    »
1234567
891011121314
15161718192021
22232425262728
2930
网站分类
标签列表
最新留言
    热门文章 | 热评文章 | 随机文章
文章归档
友情链接
  • 订阅本站的 RSS 2.0 新闻聚合
  • 扫描加本站机器视觉QQ群,验证答案为:halcon勇哥的机器视觉
  • 点击查阅微信群二维码
  • 扫描加勇哥的非标自动化群,验证答案:C#/C++/VB勇哥的非标自动化群
  • 扫描加站长微信:站长微信:abc496103864
  • 扫描加站长QQ:
  • 扫描赞赏本站:
  • 留言板:

Powered By Z-BlogPHP 1.7.2

Copyright Your skcircle.com Rights Reserved.

鄂ICP备18008319号


站长QQ:496103864 微信:abc496103864