早就聽說.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