C# async await 死锁问题总结

Posted sdbob

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C# async await 死锁问题总结相关的知识,希望对你有一定的参考价值。

可能发生死锁的程序类型

1、WPF/WinForm程序

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

 

死锁的产生原理

对异步方法返回的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代码是同样的道理。

 

如何避免死锁

可以试验一下,下面的代码是不会有问题的:

        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;

            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/");

            return httpClient.GetStringAsync("/").Result;
        }

下面的却会产生死锁:

        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/");

            return await httpClient.GetStringAsync("/");
        }

为什么在HttpClient的GetStringAsync()返回的Task上访问Resut不会产生死锁,而自己写的代码就出现了死锁?

mono的HttpClient源代码上,可以找到一些端倪:

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

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

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

这样,问题就迎刃而解了:以最初的WPF代码为例,主线程阻塞,等待Task对象“完成”;Method1被调用100ms后,在另外的线程上执行了await的之后的代码,于是Task对象完成。主线程恢复执行。

把使用HttpClient造成死锁的代码改成如下形式:

        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/");

            return await httpClient.GetStringAsync("/").ConfigureAwait(false);
        }

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

 

后话

  1. 在异步工具方法中,尽可能的加上ConfigureAwait(false),可以防止方法的使用者在异步方法返回的Task上调用Wait()或访问Result属性而造成死锁
  2. 在一些场景中,我们是需要让await之后的代码返回原先的context执行的。如,在await之后,需要访问UI控件。所以,ConfigureAwait(false)不能滥用

 

以上是关于C# async await 死锁问题总结的主要内容,如果未能解决你的问题,请参考以下文章

抓住异步编程async/await语法糖的牛鼻子

C# :异步编程的注意点

抓住异步编程async/await语法糖的牛鼻子: SynchronizationContext

研究c#异步操作async await状态机的总结

研究c#异步操作async await状态机的总结

C# 使用Awaiter