C# 5.0中引入了async 和 await。这两个关键字可以让你更方便的按照同步的方式写出异步代码。也就是说使你更方便的异步编程。
下面演示使用async,await的方式:
第一步:将 VS2010 升级到 VS2010 sp1.
第二步:下载Async CTP,进行安装
第三步:为应用程序添加AsyncCTPLibrary引用,如下:
OK,将上面的SumPageSizes 方法修改如下:
public async Task<int> SumPageSizesAsync2(IList<Uri> uris){ var tasks = uris.Select(uri => new WebClient().DownloadDataTaskAsync(uri)); var data = await TaskEx.WhenAll(tasks); return await TaskEx.Run(() => { return data.Sum(s => s.Length); });}
在AsyncCTPLibrary.dll中,微软为一些类提供了扩展,如下:
WebClient的扩展如下:
可以看到基本上为每个Download 都增加了一个XXXTaskAsync 的扩展方法。
async、await线程死锁的故事及解决方法:
早就听说.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后再无后续输出。运行结果:
看来“事与愿违”。那么这里怎么没有出现之前的问题呢?
提问:再将第25行改为Task.Run(() => Sleep(10)).Wait();这时候会输出什么呢,或者说step4的输出线程ID是多少?Task.Wait();和await不一样,它会阻塞当前线程(而不管内部逻辑是否开辟了新的线程)。运行结果:
可得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 }
解决方法:
var response= await client.PostAsync(uri, httpContent).ConfigureAwait(false);//第16行
调用方使用await调用async方法,而非GetResult、Task.Resul、Task.Wait;//第3行
使用client.PostAsync(uri, httpContent).Result.Content.ReadAsStringAsync().Result。//阻塞当前线程,而非将控制权返回给调用方,如前所述