C# 5.0中引入了async 和 await。這兩個(gè)關(guān)鍵字可以讓你更方便的按照同步的方式寫出異步代碼。也就是說使你更方便的異步編程。
下面演示使用async,await的方式:
第一步:將 VS2010 升級(jí)到 VS2010 sp1.
第二步:下載Async CTP,進(jìn)行安裝
第三步:為應(yīng)用程序添加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中,微軟為一些類提供了擴(kuò)展,如下:
WebClient的擴(kuò)展如下:
可以看到基本上為每個(gè)Download 都增加了一個(gè)XXXTaskAsync 的擴(kuò)展方法。
async、await線程死鎖的故事及解決方法:
早就聽說.Net4.5里有一對(duì)好基友a(bǔ)sync和await,今兒我迫不及待地拿過來爽了一把。尼瑪就悲劇了啊。
場(chǎng)景重構(gòu)
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 }
上述代碼是對(duì)真實(shí)案例的簡(jiǎn)化,即通過第三方OPenAPI獲取用戶信息,然后展示在Index頁中,很簡(jiǎn)單。我點(diǎn)運(yùn)行之后,發(fā)現(xiàn)執(zhí)行到var response = await client.PostAsync(uri, httpContent);黃色小箭頭進(jìn)入到這句代碼之后就消失的無影無蹤,我等了半宿,然后……然后就沒有然后了,沒有異常,只有寂寞。
我首先考慮到是不是HttpClient引起的(之前使用HttpWebRequest.GetResponse能按預(yù)期執(zhí)行,因此不會(huì)是http://www.xxxx.com/user/get這個(gè)API的問題,且當(dāng)時(shí)并沒有想到會(huì)是線程問題),查閱了很多資料,對(duì)代碼進(jìn)行反復(fù)修改,問題依舊。后來我鬼使神差地將最后兩行改為:
1 var response = client.PostAsync(uri, httpContent).Result.Content.ReadAsStringAsync().Result; 2 return response;
問題竟然神奇的消失了,當(dāng)Index頁面展現(xiàn)在我眼前的時(shí)候,我心說這不是玩我呢吧。我安慰自己說這或許是.NET框架的某個(gè)不為人知的bug,倒霉被我遇到,不管了洗洗睡吧。經(jīng)過一個(gè)晚上的折騰,累得夠嗆,于是我很快就進(jìn)入了夢(mèng)鄉(xiāng)。夢(mèng)中考英語,試卷上只能看到密密麻麻的a,我急得滿頭大汗,再仔細(xì)一看,滿滿的就兩個(gè)單詞:async和await。我一下驚醒了。
async和await
關(guān)于async和await,這兄弟倆是對(duì)異步編程的語法簡(jiǎn)化。談到異步,就涉及到線程和邏輯執(zhí)行順序,看下面代碼就一清二楚了。
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會(huì)阻塞當(dāng)前線程直到AsyncSleep返回 9 demo.AsyncSleep();//不會(huì)阻塞當(dāng)前線程 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關(guān)鍵字表示等待Task.Run傳入的邏輯執(zhí)行完畢,此時(shí)(等待時(shí))AsyncSleep的調(diào)用方能繼續(xù)往下執(zhí)行 24 //Task.Run將開辟一個(gè)新線程執(zhí)行指定邏輯 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 }
運(yùn)行結(jié)果:
注意step2和step4雖然在同一個(gè)方法內(nèi)部,但它們的運(yùn)行線程是不同的,step4與step3一樣使用Task.Run開辟的新線程。注意:假如我們?cè)赟leep里再次使用Task.Run又開辟了新線程,假設(shè)ID為10,并通過await關(guān)鍵詞修飾,那么step4將運(yùn)行在線程10。假如將第8、9行注釋互換:
1 demo.AsyncSleep().Wait();//Wait會(huì)阻塞當(dāng)前線程直到AsyncSleep返回 2 //demo.AsyncSleep();//不會(huì)阻塞當(dāng)前線程
即人為控制異步邏輯同步返回,其實(shí)這和之前獲取用戶信息的場(chǎng)景是一樣一樣的,猜想是在執(zhí)行step2或step3后再無后續(xù)輸出。運(yùn)行結(jié)果:
看來“事與愿違”。那么這里怎么沒有出現(xiàn)之前的問題呢?
提問:再將第25行改為Task.Run(() => Sleep(10)).Wait();這時(shí)候會(huì)輸出什么呢,或者說step4的輸出線程ID是多少?Task.Wait();和await不一樣,它會(huì)阻塞當(dāng)前線程(而不管內(nèi)部邏輯是否開辟了新的線程)。運(yùn)行結(jié)果:
可得step4仍運(yùn)行在主線程。
線程死鎖
引起線程死鎖的原因有很多。在ASP.NET[ MVC]的場(chǎng)景中,涉及到一個(gè)概念就是AspNetSynchronizationContext,它同時(shí)只能被一個(gè)線程獨(dú)占。結(jié)合async和await的特性,回到本文開頭的代碼:
1 public ActionResult Index(string ucode) 2 { 3 string userInfo = GetUserInfo(ucode).Result;//線程A阻塞,等待GetUserInfo返回,當(dāng)前上下文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在其內(nèi)部開辟新線程(設(shè)為B)異步執(zhí)行,注意await并不會(huì)阻塞當(dāng)前線程,而是將控制權(quán)返回方法調(diào)用方,這里是Index Action 16 var response = await client.PostAsync(uri, httpContent); //client.PostAsync返回,但下列代碼仍運(yùn)行在線程B。當(dāng)前方法企圖重入AspNetSynchronizationContext,死鎖產(chǎn)生在這里17 return response.Content.ReadAsStringAsync().Result; 18 }
解決方法:
var response= await client.PostAsync(uri, httpContent).ConfigureAwait(false);//第16行
調(diào)用方使用await調(diào)用async方法,而非GetResult、Task.Resul、Task.Wait;//第3行
使用client.PostAsync(uri, httpContent).Result.Content.ReadAsStringAsync().Result。//阻塞當(dāng)前線程,而非將控制權(quán)返回給調(diào)用方,如前所述