async、await在ASP.NET[ MVC]中之线程死锁的故事

简介: 早就听说.Net4.5里有一对好基友async和await,今儿我迫不及待地拿过来爽了一把。尼玛就悲剧了啊。 场景重构 1 public ActionResult Index(string ucode) 2 { 3 string userInfo = GetUserInfo(ucode).

早就听说.Net4.5里有一对好基友async和await,今儿我迫不及待地拿过来爽了一把。尼玛就悲剧了啊。

场景重构

 1 public ActionResult Index(string ucode)
 2 {
 3     string userInfo = GetUserInfo(ucode).Result;
 4     ViewData["UserInfo"] = userInfo;
 5     return View();
 6 }
 7 
 8 async Task<string> GetUserInfo(string ucode)
 9 {
10     HttpClient client = new HttpClient();
11     var httpContent = new FormUrlEncodedContent(new Dictionary<string, string>()
12     {
13         {"ucode", ucode}
14     });
15     string uri = "http://www.xxxx.com/user/get";
16     var response = await client.PostAsync(uri, httpContent);
17     return response.Content.ReadAsStringAsync().Result;
18 }

上述代码是对真实案例的简化,即通过第三方OPenAPI获取用户信息,然后展示在Index页中,很简单。我点运行之后,发现执行到var response = await client.PostAsync(uri, httpContent);黄色小箭头进入到这句代码之后就消失的无影无踪,我等了半宿,然后……然后就没有然后了,没有异常,只有寂寞。

我首先考虑到是不是HttpClient引起的(之前使用HttpWebRequest.GetResponse能按预期执行,因此不会是http://www.xxxx.com/user/get这个API的问题,且当时并没有想到会是线程问题),查阅了很多资料,对代码进行反复修改,问题依旧。后来我鬼使神差地将最后两行改为:

1 var response = client.PostAsync(uri, httpContent).Result.Content.ReadAsStringAsync().Result;
2 return response;

问题竟然神奇的消失了,当Index页面展现在我眼前的时候,我心说这不是玩我呢吧。我安慰自己说这或许是.NET框架的某个不为人知的bug,倒霉被我遇到,不管了洗洗睡吧。经过一个晚上的折腾,累得够呛,于是我很快就进入了梦乡。梦中考英语,试卷上只能看到密密麻麻的a,我急得满头大汗,再仔细一看,满满的就两个单词:async和await。我一下惊醒了。

async和await

关于async和await,这兄弟俩是对异步编程的语法简化。谈到异步,就涉及到线程和逻辑执行顺序,看下面代码就一清二楚了。

 1 class Program
 2 {
 3     static void Main(string[] args)
 4     {
 5         Console.WriteLine("step1,线程ID:{0}", System.Threading.Thread.CurrentThread.ManagedThreadId);
 6 
 7         AsyncDemo demo = new AsyncDemo();
 8         //demo.AsyncSleep().Wait();//Wait会阻塞当前线程直到AsyncSleep返回
 9         demo.AsyncSleep();//不会阻塞当前线程
10 
11         Console.WriteLine("step5,线程ID:{0}", System.Threading.Thread.CurrentThread.ManagedThreadId);
12         Console.ReadLine();
13     }
14 }
15 
16 public class AsyncDemo
17 {
18 
19     public async Task AsyncSleep()
20     {
21         Console.WriteLine("step2,线程ID:{0}", System.Threading.Thread.CurrentThread.ManagedThreadId);
22 
23         //await关键字表示“等待”Task.Run传入的逻辑执行完毕,此时(等待时)AsyncSleep的调用方能继续往下执行(准确地说,是当前线程不会被阻塞)
24         //Task.Run将开辟一个新线程执行指定逻辑
25         await Task.Run(() => Sleep(10));
26 
27         Console.WriteLine("step4,线程ID:{0}", System.Threading.Thread.CurrentThread.ManagedThreadId);
28     }
29 
30     private void Sleep(int second)
31     {
32         Console.WriteLine("step3,线程ID:{0}", System.Threading.Thread.CurrentThread.ManagedThreadId);
33 
34         Thread.Sleep(second * 1000);
35     }
36 
37 }

运行结果:

注意step2和step4虽然在同一个方法内部,但它们的运行线程是不同的,step4与step3一样使用Task.Run开辟的新线程。注意:假如我们在Sleep里再次使用Task.Run又开辟了新线程,假设ID为10,并通过await关键词修饰,那么step4将运行在线程10。假如将第8、9行注释互换:

1 demo.AsyncSleep().Wait();//Wait会阻塞当前线程直到AsyncSleep返回
2 //demo.AsyncSleep();//不会阻塞当前线程

即人为控制异步逻辑同步返回,其实这和之前获取用户信息的场景是一样一样的,猜想是在执行step2或step3后再无后续输出。运行结果:

看来“事与愿违”。那么之前的出现的问题是怎么回事呢?既然step4和step1所在线程不一样,我们能想到什么?当然是线程死锁了!

提问:再将第25行改为Task.Run(() => Sleep(10)).Wait();这时候会输出什么呢,或者说step4的输出线程ID是多少?Task.Wait();和await不一样,它会阻塞当前线程(而不管内部逻辑是否开辟了新的线程)。运行结果:

可得step4仍运行在主线程。

线程死锁

引起线程死锁的原因有很多。在ASP.NET[ MVC]的场景中,涉及到一个概念就是AspNetSynchronizationContext。AspNetSynchronizationContext出现在.NET Framework 2.0中,因为这个版本在 ASP.NET 体系结构中引入了异步页面在 .NET Framework 2.0 之前的版本中,每个 ASP.NET 请求都需要一个线程,直到该请求完成。 这会造成线程利用率低下,因为页面逻辑通常依赖于数据库查询和 Web 服务调用,并且处理请求的线程必须等待,直到所有这些操作结束。 使用异步页面,处理请求的线程可以开始每个操作,然后返回到 ASP.NET 线程池,当操作结束时,ASP.NET 线程池的另一个线程可以完成该请求,AspNetSynchronizationContext在这个过程中扮演了异步操作周期维护员的角色(或许还发挥了其它作用)。当一个异步操作完成,需要依赖AspNetSynchronizationContext告知页面,此时AspNetSynchronizationContext将未完成的异步操作数减1,并以同步方式处理异步线程发送过来的委托(即便是以Post“异步”方法),因此假如一个页面请求有多个异步操作同时完成,每次也只能执行一个回调委托(不同委托执行的线程不知是否是同一个,however,执行线程将具有原始页面的标识和区域)。综上所述,同一个AspNetSynchronizationContext(不知道一个AspNetSynchronizationContext实例是针对单个请求还是整个应用程序同时只能最多被一个线程使用,结合async和await的特性,回到本文开头的代码:

 1 public ActionResult Index(string ucode)
 2 {
 3     string userInfo = GetUserInfo(ucode).Result;//线程A阻塞,等待GetUserInfo返回,当前上下文AspNetSynchronizationContext
 4     ViewData["UserInfo"] = userInfo;
 5     return View();
 6 }
 7 
 8 async Task<string> GetUserInfo(string ucode)
 9 {
10     HttpClient client = new HttpClient();
11     var httpContent = new FormUrlEncodedContent(new Dictionary<string, string>()
12     {
13         {"ucode", ucode}
14     });
15     string uri = "http://www.xxxx.com/user/get";     //client.PostAsync在其内部开辟新线程(设为B)异步执行,注意await并不会阻塞当前线程,而是将控制权返回方法调用方,这里是Index Action
16     var response = await client.PostAsync(uri, httpContent);     //client.PostAsync返回,但下列代码仍运行在线程B。当前方法企图重入AspNetSynchronizationContext,死锁产生在这里
17     return response.Content.ReadAsStringAsync().Result;
18 }

 解决方法:

  1. var response= await client.PostAsync(uri, httpContent).ConfigureAwait(false);//第16行
  2. 调用方使用await调用async方法,而非GetResult、Task.Resul、Task.Wait;//第3行
  3. 使用client.PostAsync(uri, httpContent).Result.Content.ReadAsStringAsync().Result。//阻塞当前线程,而非将控制权返回给调用方,如前所述

参考资料


 

后记

await关键字并不表示后续代码马上在新线程上执行,是否开辟线程取决于是否真正创建了Task(or 从Task池中取得)。运行下面代码:

 1 class Program
 2 {
 3     static void Main(string[] args)
 4     {
 5         Console.WriteLine($"1:Thread.CurrentThread.ManagedThreadId-{Thread.CurrentThread.ManagedThreadId}");
 6         TestTransfer1();
 7         Console.WriteLine($"8:Thread.CurrentThread.ManagedThreadId-{Thread.CurrentThread.ManagedThreadId}");
 8         Console.ReadLine();
 9     }
10 
11     static async void TestTransfer1()
12     {
13         Console.WriteLine($"2:Thread.CurrentThread.ManagedThreadId-{Thread.CurrentThread.ManagedThreadId}");
14         await TestTransfer2();
15         Console.WriteLine($"7:Thread.CurrentThread.ManagedThreadId-{Thread.CurrentThread.ManagedThreadId}");
16     }
17 
18     static async Task TestTransfer2()
19     {
20         Console.WriteLine($"3:Thread.CurrentThread.ManagedThreadId-{Thread.CurrentThread.ManagedThreadId}");
21         await Test();
22         Console.WriteLine($"6:Thread.CurrentThread.ManagedThreadId-{Thread.CurrentThread.ManagedThreadId}");
23     }
24 
25     static async Task Test()
26     {
27         Console.WriteLine($"4:Thread.CurrentThread.ManagedThreadId-{Thread.CurrentThread.ManagedThreadId}");
28         await Task.Run(() => Sleep(5)); //此处之后才开辟了新线程
29         Console.WriteLine($"5:Thread.CurrentThread.ManagedThreadId-{Thread.CurrentThread.ManagedThreadId}");
30     }
31 
32     static void Sleep(int second)
33     {
34         Thread.Sleep(second * 1000);
35     }
36 }

运行结果:

一目了然,所以我们不需要担心多级方法调用时会创建众多线程并切换导致的性能问题。

.NET平台提供的异步方法一般都会new或get一个Task,因此会如上代码一样遇到这些方法,后续逻辑会切换到新线程上运行。需要注意的是.NET可能会在某些方面做一些优化,比如以同步方式完成此类方法,比如StreamWriter.WriteLineAsync方法,我测试了之后还是运行在原线程,maybe其内部是根据写入字符多少决定是否切换线程,这就不深究了。

关于是否在await后才开始真正执行异步方法,改造上面代码如下:

 1 class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             TestTransfer1();
 6             Console.ReadLine();
 7         }
 8 
 9         static async void TestTransfer1()
10         {
11             Console.WriteLine($"1:Thread.CurrentThread.ManagedThreadId-{Thread.CurrentThread.ManagedThreadId}");
12             var task = Test();            
13             Sleep(2);
14             Console.WriteLine($"4:Thread.CurrentThread.ManagedThreadId-{Thread.CurrentThread.ManagedThreadId}");
15             await task;
16         }
17 
18         static async Task Test()
19         {
20             Console.WriteLine($"2:Thread.CurrentThread.ManagedThreadId-{Thread.CurrentThread.ManagedThreadId}");
21             await Task.Run(() => Sleep(1)); //此处之后才开辟了新线程
22             Console.WriteLine($"3:Thread.CurrentThread.ManagedThreadId-{Thread.CurrentThread.ManagedThreadId}");
23         }
24 
25         static void Sleep(int second)
26         {
27             Thread.Sleep(second * 1000);
28         }
29     }

运行结果:

可知在获取task实例时,异步操作就开始了,而不需要等await。由于这个特性,我们可以发起多个没有顺序依赖关系的task,最后再统一await它们,提高效率,比如分页:

var task_totalcount = query.CountAsync();               
query = query.OrderBy(sortfield, sortorder);
query = query.Skip(startindex).Take(takecount);
var task_getdata = query.ToListAsync();

result.TotalCount = await task_totalcount;
result.Data = await task_getdata;

return result;

 

参考资料:

C#与C++的发展历程第三 - C#5.0异步编程巅峰

 

转载请注明本文出处:http://www.cnblogs.com/newton/archive/2013/05/13/3075039.html

目录
相关文章
|
23天前
|
开发框架 Java .NET
.net core 非阻塞的异步编程 及 线程调度过程
【11月更文挑战第12天】本文介绍了.NET Core中的非阻塞异步编程,包括其基本概念、实现方式及应用示例。通过`async`和`await`关键字,程序可在等待I/O操作时保持线程不被阻塞,提高性能。文章还详细说明了异步方法的基础示例、线程调度过程、延续任务机制、同步上下文的作用以及如何使用`Task.WhenAll`和`Task.WhenAny`处理多个异步任务的并发执行。
|
2月前
|
安全 Java 程序员
【多线程-从零开始-肆】线程安全、加锁和死锁
【多线程-从零开始-肆】线程安全、加锁和死锁
48 0
|
3月前
|
开发框架 前端开发 JavaScript
ASP.NET MVC 教程
ASP.NET 是一个使用 HTML、CSS、JavaScript 和服务器脚本创建网页和网站的开发框架。
45 7
|
3月前
|
存储 开发框架 前端开发
ASP.NET MVC 迅速集成 SignalR
ASP.NET MVC 迅速集成 SignalR
72 0
|
4月前
|
开发框架 前端开发 .NET
ASP.NET MVC WebApi 接口返回 JOSN 日期格式化 date format
ASP.NET MVC WebApi 接口返回 JOSN 日期格式化 date format
53 0
|
4月前
|
开发框架 前端开发 安全
ASP.NET MVC 如何使用 Form Authentication?
ASP.NET MVC 如何使用 Form Authentication?
|
4月前
|
Java
Java多线程-死锁的出现和解决
死锁是指多线程程序中,两个或以上的线程在运行时因争夺资源而造成的一种僵局。每个线程都在等待其中一个线程释放资源,但由于所有线程都被阻塞,故无法继续执行,导致程序停滞。例如,两个线程各持有一把钥匙(资源),却都需要对方的钥匙才能继续,结果双方都无法前进。这种情况常因不当使用`synchronized`关键字引起,该关键字用于同步线程对特定对象的访问,确保同一时刻只有一个线程可执行特定代码块。要避免死锁,需确保不同时满足互斥、不剥夺、请求保持及循环等待四个条件。
|
前端开发 数据安全/隐私保护
net MVC中的模型绑定、验证以及ModelState
net MVC中的模型绑定、验证以及ModelState 模型绑定 模型绑定应该很容易理解,就是传递过来的数据,创建对应的model并把数据赋予model的属性,这样model的字段就有值了。
1697 0
|
4月前
|
开发框架 .NET
Asp.Net Core 使用X.PagedList.Mvc.Core分页 & 搜索
Asp.Net Core 使用X.PagedList.Mvc.Core分页 & 搜索
136 0
|
7月前
|
开发框架 前端开发 .NET
ASP.NET CORE 3.1 MVC“指定的网络名不再可用\企图在不存在的网络连接上进行操作”的问题解决过程
ASP.NET CORE 3.1 MVC“指定的网络名不再可用\企图在不存在的网络连接上进行操作”的问题解决过程
208 0