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 }

运行结果:async、await在ASP.NET[ MVC]中之线程死锁的故事注意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后再无后续输出。运行结果:async、await在ASP.NET[ MVC]中之线程死锁的故事,看来“事与愿违”。那么之前的出现的问题是怎么回事呢?既然step4和step1所在线程不一样,我们能想到什么?当然是线程死锁了!

提问:再将第25行改为Task.Run(() => Sleep(10)).Wait();这时候会输出什么呢,或者说step4的输出线程ID是多少?Task.Wait();和await不一样,它会阻塞当前线程(而不管内部逻辑是否开辟了新的线程)。运行结果:async、await在ASP.NET[ MVC]中之线程死锁的故事,可得step4仍运行在主线程。

线程死锁

引起线程死锁的原因有很多。在ASP.NET[ MVC]的场景中,涉及到一个概念就是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。//阻塞当前线程,而非将控制权返回给调用方,如前所述

参考资料

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

你可能感兴趣的:(asp.net)