C# async await 死锁问题总结

為{幸葍}努か 提交于 2020-01-06 20:39:44

可能发生死锁的程序类型

1、WPF/WinForm程序

2、asp.net (不包括asp.net core)程序

 

死锁的产生原理

对异步方法返回的Task调用Wait()或访问Result属性时,可能会产生死锁。

下面的WPF代码会出现死锁:

        private void Button_Click_7(object sender, RoutedEventArgs e)
        {
            Method1().Wait();
        }

        private async Task Method1()
        {
            await Task.Delay(100);

            txtLog.AppendText("后续代码");
        }

下面的asp.net mvc代码也会出现死锁:

        public ActionResult Index()
        {
            string s=Method1().Result;

            return View();
        }

        private async Task<string> Method1()
        {
            await Task.Delay(100);

            return "hello";
        }

以WPF代码为例,事件处理器调用Method1,得到Task对象,然后调用Task的Wait方法,阻塞自己所在的线程,即主线程,直到Task对象“完成”。而返回的Task对象要想“完成”,必须在主线程上执行await之后的代码。而主线程早就处于阻塞状态,它在等待Task对象完成!于是死锁就产生了。

asp.net mvc代码是同样的道理。

 

如何避免死锁

从上面的两个例子中似乎可以得出结论:在WPF/WinForm/asp.net程序中,在异步方法上调用.Result/Wait(),就会产生死锁。

写一段从web获取数据并显示在文本框中的WPF代码(此代码仅为举例说明,异步事件处理器才是正道):

        private void Button_Click_8(object sender, RoutedEventArgs e)
        {
            HttpClient httpClient = new HttpClient();
            httpClient.BaseAddress = new Uri("https://www.baidu.com/");

            string html = httpClient.GetStringAsync("/").Result;            html = "【" + html + "】";

            txtLog.AppendText(html);
        }

试验一下,竟然没出现死锁。

把获取数据的代码摘出来吧:

        private void Button_Click_8(object sender, RoutedEventArgs e)
        {
            string html = GetHtml();

            txtLog.AppendText(html);
        }

        private string GetHtml()
        {
            HttpClient httpClient = new HttpClient();
            httpClient.BaseAddress = new Uri("https://www.baidu.com/");

            string html=httpClient.GetStringAsync("/").Result;       html = "【" + html + "】";                   return html;
        }

完全没问题,这是肯定的。

GetHtml()可以写成异步方法,再改一下:

        private void Button_Click_8(object sender, RoutedEventArgs e)
        {
            string html = GetHtml().Result;

            txtLog.AppendText(html);
        }

        private async Task<string> GetHtml()
        {
            HttpClient httpClient = new HttpClient();
            httpClient.BaseAddress = new Uri("https://www.baidu.com/");
            string html=await httpClient.GetStringAsync("/");              
            html = "【" + html + "】";
           return html;        }

这时,死锁出现了。

为什么在HttpClient的GetStringAsync()方法上执行.Result不会死锁,而在自己写的异步方法上执行.Result,就出现了死锁?难道HttpClient的GetStringAsync()方法内部有什么特殊的处理?

看一下mono的HttpClient源代码,可以发现:

所有await 表达式后面,都加了ConfigureAwait (false),如

return await resp.Content.ReadAsStringAsync ().ConfigureAwait (false);

而由Task的msdn文档可以知,ConfigureAwait (false)会指示await之后的代码不在原先的context (可理解为线程)上运行。

修改一下GetHtml()异步方法的代码:

        private void Button_Click_8(object sender, RoutedEventArgs e)
        {
            string html = GetHtml().Result;

            txtLog.AppendText(html);
        }
        private async Task<string> GetHtml()
        {
            HttpClient httpClient = new HttpClient();
            httpClient.BaseAddress = new Uri("https://www.baidu.com/");
            string html=await httpClient.GetStringAsync("/").ConfigureAwait(false);              
            html = "【" + html + "】";
           return html;        }
 

可以发现,死锁不会出现了。

分析:GetHtml()被调用后,主线程阻塞,等待Task对象“完成”;HttpClient获取数据完毕,在另外的线程上执行了await的之后的代码,于是Task对象完成。主线程恢复执行。(注意,即使“await之后没有代码”,即GetHtml()方法体中直接写return await httpClient.GetStringAsync("/"),也是需要加.ConfigureAwait(false)的)

总结

异步编程有很多好处,尤其在web程序中,可以有效避免“线程饥饿”,大幅提高网站的吞吐量。异步编程的最佳做法当然是“一路异步下去(async all the way)”,然而可能有的公司同事对异步编程不太熟悉,或者有的地方不适合用异步代码,这些情况下都可能会选择在异步方法返回的Task上调用Wait()或访问Result属性,毕竟这是Task对象天然的特性。一旦需要阻塞异步代码,就要警惕死锁的问题了。

asyn/await的编程方式,默认就是await之后的代码会返回原先的context执行。如果await之后是访问UI控件的代码,则必须在UI线程上执行,asyn/await这种默认的设计是很合适的,这可以让我们轻松地写出同步风格的异步代码。如果await之后的代码不需要返回原先的context执行,例如,仅仅是执行Http请求,获取和处理数据,那么完全可以加上ConfigureAwait(false)。

有的类库,如常用的HttpClient,仅提供了异步方法。有时我们会想使用第三方类库来写一段公用的代码,做成公用的工具方法。这时候最佳的做法就是:工具方法做成异步的,使用await调用第三方类库的异步方法,后面添加ConfigureAwait(false),并且保证在工具方法中不会有访问UI控件的代码。这样可以维持异步方法带来的好处,并且可以保证一旦工具方法的使用者阻塞异步代码时,不会产生死锁。

 

附加  async/await学习资料

 C# Under the Hood: async/await  作者从动手写一个“可等待”的方法开始,进而通过反编译工具分析异步方法生成的的实质代码,揭示了async/await的本质——回调

What happens in an async method  msdn编程指南,图示异步方法的执行流程

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!