服务器之家:专注于VPS、云服务器配置技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - C# - 浅谈C# async await 死锁问题总结

浅谈C# async await 死锁问题总结

2022-10-11 14:45Bob1227 C#

这篇文章主要介绍了浅谈C# async await 死锁问题总结,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

可能发生死锁的程序类型

1、WPF/WinForm程序

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

死锁的产生原理

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

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

?
1
2
3
4
5
6
7
8
9
10
11
private void Button_Click_7(object sender, RoutedEventArgs e)
    {
      Method1().Wait();
    }
 
    private async Task Method1()
    {
      await Task.Delay(100);
 
      txtLog.AppendText("后续代码");
    }

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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
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(),就会产生死锁。然而事实并非如此。

如下面的WPF代码就不会出现死锁:(从web获取数据并显示在文本框中。此代码仅为举例说明,异步事件处理器才是正道)

?
1
2
3
4
5
6
7
8
9
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);
    }

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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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()可以写成异步方法,再改一下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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()方法是异步方法,我们调用它,然后用async/await的方式创建了一个自己的异步方法。先不“一路异步到底(Async All the Way)”。)

运行一下,死锁出现了。

为什么在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()异步方法的代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
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)的)

当然,如果事件处理器是异步的,即使不加.ConfigureAwait(false),也不会有任何问题:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private async void Button_Click_8(object sender, RoutedEventArgs e)
    {
      string html = await GetHtml();
 
      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;
    }

试想一下,如果GetHtml()被放到单独的类中,做成类库,那么,里面如果不加.ConfigureAwait(false),则只能假设使用这个类库的人严格遵循异步编程规范了。一旦使用者在GetHtml()上执行.Result,死锁就无可避免了。

仔细看HttpClient的源代码,可以发现,它的GetStringAsync()方法也并不是“天生的”异步方法,它也是用await运算符调用了自己的其他的异步方法,并且在每次调用后都添加了.ConfigureAwait(false)。

那么,最初的WPF程序的死锁是否可以用.ConfigureAwait(false)解决呢?注意,txtLog是一个文本框,UI控件只能在UI线程访问,所以添加上.ConfigureAwait(false)后会报错:“InvalidOperationException: 调用线程无法访问此对象,因为另一个线程拥有该对象”。那么是否可以改成这样:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
private void Button_Click_7(object sender, RoutedEventArgs e)
    {
      Method1().Wait();
    }
 
    private async Task Method1()
    {
      await Task.Delay(100).ConfigureAwait(false);
 
      Dispatcher.Invoke(() => {
        txtLog.AppendText("后续代码");
      });
    }

依然是死锁。所以,乖乖的用异步事件处理器吧:

?
1
2
3
4
5
6
7
8
9
10
11
private async void Button_Click_7(object sender, RoutedEventArgs e)
    {
      await Method1();
    }
 
    private async Task Method1()
    {
      await Task.Delay(100);
 
      txtLog.AppendText("后续代码");
    }

上面的代码还说明一个问题:在异步工具方法中,不要写访问UI控件的代码,否则无法规避死锁问题。

总结

  • 死锁会发生在不遵循异步编程规范——在异步方法返回的Task对象上执行Wait()或.Result时
  • ConfigureAwait(false)指定await后的代码不返回原先的context,可以避免死锁
  • 如果await之后的代码不需要返回原先的context执行,例如,仅仅是执行Http请求,获取和处理数据,那么完全可以加上ConfigureAwait(false)。
  • 如果作为类库的创作者,编写异步方法时,应尽可能的使用ConfigureAwait(false),以保证一旦类库的使用者阻塞异步方法时,不会产生死锁。
  • 在异步类库/工具方法中,应避免加入访问UI控件的代码 

附加  async/await学习资料

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

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

到此这篇关于浅谈C# async await 死锁问题总结的文章就介绍到这了,更多相关C# async await 死锁内容请搜索服务器之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持服务器之家!

原文链接:https://www.cnblogs.com/sdBob/p/12151013.html

延伸 · 阅读

精彩推荐
  • C#C#动态创建button按钮的方法实例详解

    C#动态创建button按钮的方法实例详解

    这篇文章主要介绍了C#动态创建button按钮的方法实例详解的相关资料,需要的朋友可以参考下...

    123si5532022-01-06
  • C#Unity Shader实现图形绘制(蓝天白云大海)

    Unity Shader实现图形绘制(蓝天白云大海)

    这篇文章主要为大家详细介绍了Unity Shader实现图形绘制,蓝天白云大海,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考...

    ZzEeRO3892022-09-06
  • C#关于C#转换二进制所引起的一些思考

    关于C#转换二进制所引起的一些思考

    这篇文章主要给大家介绍了关于C#转换二进制所引起的一些思考,文中通过示例代码介绍的非常详细,对大家的学习或者使用C#具有一定的参考学习价值,需...

    Jeffcky5752022-09-21
  • C#C#学习笔记- 浅谈数组复制,排序,取段,元组

    C#学习笔记- 浅谈数组复制,排序,取段,元组

    下面小编就为大家带来一篇C#学习笔记- 浅谈数组复制,排序,取段,元组。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看...

    C#教程网4602021-12-03
  • C#C#实现简单的Login窗口实例

    C#实现简单的Login窗口实例

    这篇文章主要介绍了C#实现简单的Login窗口,实例分析了C#显示及关闭登陆Login窗口的技巧,非常具有实用价值,需要的朋友可以参考下...

    我心依旧5502021-10-21
  • C#C#获取Excel第一列的实例方法

    C#获取Excel第一列的实例方法

    在本篇文章里小编给各位分享了关于C# 怎么获取Excel第一列的实例方法和代码,需要的朋友们可以学习下。...

    fransiswu10642022-07-31
  • C#C#RSA对接JAVA中RSA方式代码实例

    C#RSA对接JAVA中RSA方式代码实例

    在本篇文章中小编给各位整理的是关于C#RSA对接JAVA中RSA方式代码实例,有需要的朋友们可以参考一下。...

    盛开的雨季5912022-08-04
  • C#C#裁剪,缩放,清晰度,水印处理操作示例

    C#裁剪,缩放,清晰度,水印处理操作示例

    这篇文章主要为大家详细介绍了C#裁剪,缩放,清晰度,水印处理操作示例,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...

    吴 剑7432021-12-08