async、await在ASP.NET[ MVC]中之线程死锁的故事
早就听说.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 }
解决方法:
var response= await client.PostAsync(uri, httpContent).ConfigureAwait(false);//第16行
- 调用方使用
await调用async方法
,而非GetResult、
Task.Resul、T
ask.Wait;//第3行
- 使用client.PostAsync(uri, httpContent).Result.Content.ReadAsStringAsync().Result。//阻塞当前线程,而非将控制权返回给调用方,如前所述
参考资料
- Asynchronous Programming with Async and Await (C# and Visual Basic)
- HttpClient, HttpClientHandler, and WebRequestHandler Explained
- HttpClient.GetAsync(…) never returns when using await/async
- SynchronizationContext 综述(将请求头的首选语言改为英文即为英文原版,对照着看比较好)
- 线程之间的通讯---SynchronizationContext
- ExecutionContext & SynchronizationContext
- 细说ASP.NET的各种异步操作
- 异步性能:了解 Async 和 Await 的成本(英文原版)——必要时减少Task对象数量(重复使用已有的Task对象或使用async void签名的异步方法等,后者将不会产生Task对象);避免SynchronizationContext的无意义封送;使用Task.WhenAll()等
后记
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;
参考资料:
转载请注明本文出处:http://www.cnblogs.com/newton/archive/2013/05/13/3075039.html