本隨筆續接:.NET 實現並行的幾種方式(三) 八、await、async - 非同步方法的秘密武器 1) 使用async修飾符 和 await運算符 輕易實現非同步方法 前三篇隨筆已經介紹了多種方式、利用多線程、充分利用多核心CPU以提高運行效率。但是以前的方式在WebAPI和GUI系統上、 使用起來 ...
本隨筆續接:.NET 實現並行的幾種方式(三)
八、await、async - 非同步方法的秘密武器
1) 使用async修飾符 和 await運算符 輕易實現非同步方法
前三篇隨筆已經介紹了多種方式、利用多線程、充分利用多核心CPU以提高運行效率。但是以前的方式在WebAPI和GUI系統上、
使用起來還是有些繁瑣,尤其是在需要上下文的情況下。而await/async就是在這樣的情況下應運而生,並且它可以在理論上讓CPU跑到100%。
async修飾符:它用以修飾方法、lambda表達式、匿名方法,以標記方法為非同步方法。非同步方法必須遵循的規範如下:
1、返回值僅且僅有三種: void、Task、Task<T>.
2、方法參數不可以使用 ref、out類型參數。
await運算符:它用以標記一個系統可在其上恢復執行的掛起點。該運算符會告訴computer不會再往下繼續執行該方法、直到等待的非同步方法執行完畢為止。同時會將程式的控制權return給其調用者。await表達式不阻止正在執行它的線程。 而是讓編譯器將非同步方法剩餘部分註冊為等待任務的延續任務。 當等待任務完成時,它會調用其延續任務,如同在掛起點上恢復執行。
2)簡單Demo
// Three things to note in the signature: // - The method has an async modifier. // - The return type is Task or Task<T>. (See "Return Types" section.) // Here, it is Task<int> because the return statement returns an integer. // - The method name ends in "Async." async Task<int> AccessTheWebAsync() { // You need to add a reference to System.Net.Http to declare client. HttpClient client = new HttpClient(); // GetStringAsync returns a Task<string>. That means that when you await the // task you'll get a string (urlContents). Task<string> getStringTask = client.GetStringAsync("http://msdn.microsoft.com"); // You can do work here that doesn't rely on the string from GetStringAsync. DoIndependentWork(); // The await operator suspends AccessTheWebAsync. // - AccessTheWebAsync can't continue until getStringTask is complete. // - Meanwhile, control returns to the caller of AccessTheWebAsync. // - Control resumes here when getStringTask is complete. // - The await operator then retrieves the string result from getStringTask. string urlContents = await getStringTask; // The return statement specifies an integer result. // Any methods that are awaiting AccessTheWebAsync retrieve the length value. return urlContents.Length; }await/async demo
3)直觀的順序圖
該圖出自: https://msdn.microsoft.com/zh-cn/library/mt674882.aspx
4) await async編程最佳做法
1、非同步方法儘量少用 void類型返回值、替代方案 使用Task類型,特例:非同步事件處理函數使用void類型
原因1、async void 無法使用try ... catch進行異常捕獲,它的異常會在上下文中引發。捕獲該種異常的方式為在GUI或web系統中使用
AppDomain.UnhandledException 進行全局異常捕獲。對於需要進隊異常進行處理的地方、這將是個災難。
原因2、async void 方法、不可以“方便”的知道其什麼時候完成,這對於超過50%的非同步方法而言、將是滅頂之災。而 async Task
可以配合 await、await Task.WhenAny、await Task.WhenAll、await Task.Delay、await Task.Yield 方便的進行後續的任務處理工作。
特例、因為事件本身是不需要返回值的,並且事件的異常也會在上下文中引發、這是合理的。所以非同步的事件處理函數使用void類型。
2、推薦一直使用async,而不要混合使用阻塞和非同步(async)避免死鎖, 特例:Main方法
使用混合編程的死鎖demo
public static class DeadlockDemo { private static async Task DelayAsync() { await Task.Delay(1000); } // This method causes a deadlock when called in a GUI or ASP.NET context. public static void Test() { // Start the delay. var delayTask = DelayAsync(); // Wait for the delay to complete. delayTask.Wait(); } }DeadlockDemo
當在GUI或者web上執行(具有上下文的環境中),會導致死鎖。這種死鎖的根本原因是 await 處理上下文的方式。 預設情況下,當等待未完成的 Task 時,會捕獲當前“上下文”,在 Task 完成時使用該上下文恢復方法的執行。 此“上下文”是當前 SynchronizationContext(除非它是 null,這種情況下則為當前 TaskScheduler)。 GUI 和 ASP.NET 應用程式具有 SynchronizationContext,它每次僅允許一個代碼區塊運行。 當 await 完成時,它會嘗試在捕獲的上下文中執行 async 方法的剩餘部分。 但是該上下文已含有一個線程,該線程在(同步)等待 async 方法完成。 它們相互等待對方,從而導致死鎖。
特例:Main方法是不可用async修飾符進行修飾的(編譯不通過)。
執行以下操作… | 阻塞式操作… | async的替換操作 |
檢索後臺任務的結果 | Task.Wait 或 Task.Result | await |
等待任何任務完成 | Task.WaitAny | await Task.WhenAny |
檢索多個任務的結果 | Task.WaitAll | await Task.WhenAll |
等待一段時間 | Thread.Sleep | await Task.Delay |
3、如何可以,請用ConfigureAwait 忽略上下文
上文也說過了,當非同步任務完成後、它會嘗試在之前的上下文環境中恢復執行。這樣帶來的問題是時間片會被切分成更多、造成更多的線程調度上的性能損耗。
一旦時間片被切分的過多、尤其是在GUI和Web具有上下文環境中運行,影響會更大。
另外,使用ConfigureAwait忽略上下文後、可避免死鎖。 因為當等待完成時,它會嘗試線上程池上下文中執行 async 方法的剩餘部分,不會存線上程等待。
5)疑問:關於 await的使用次數 和 使用的線程數量 之間的關係
使用一個await運算符,就一定會使用一個新的線程嗎? 答案:不是的。
前文已經介紹過,await運算符是依賴Task完成非同步的、並且將後續代碼至於Task的延續任務之中(這一點是編譯器搞得怪、生成了大量的模板代碼來實現該功能)。
因此,編譯器以await為分割點,將前一部分的等待任務和後一部分的延續任務分割到兩個線程之中。
前一部分的等待任務:該部分是Task依賴調度器(TaskScheduler)、從線程池中分配的工作線程。
而後一部分的延續任務:該部分所運行的線程取決於兩點:第一點,Task等待任務在運行之前捕獲的上下文環境,第二點:是否使用ConfigureAwait (false)
忽略了之前捕獲的上下文。如果沒有忽略上下文並且之前捕獲的上下文環境為:SynchronizationContext(即 GUI UI線程 或 Web中具有HttpContext的線程環境)
則 延續任務繼續在 SynchronizationContext 上下文環境中運行,否則 將使用調度器(TaskScheduler)從線程池中獲取線程來運行。
另外註意:調度器從線程池中獲取的線程、並不一定是新的,即使在迴圈連續使用多次(如果任務很快完成),那麼也有可能多次都使用同一個線程。
測試demo:
/// <summary> /// 在迴圈中使用await, 觀察使用的線程數量 /// </summary> /// <returns></returns> public async Task ForMethodAsync() { // 休眠 // await Task.Delay(TimeSpan.FromMilliseconds(100)).ConfigureAwait(false); // await Task.Delay(TimeSpan.FromMilliseconds(100)); for (int i = 0; i < 5; i++) { await Task.Run(() => { // 列印線程id PrintThreadInfo("ForMethodAsync", i.ToString()); }); } }在迴圈中使用await, 觀察使用的線程數量
上述demo在運行多次後,可能會得到上述結果:5次迴圈使用的是同一個線程,線程id為16,UI線程id為10。
結論:await的使用次數 大於 使用的線程數量,也有可能、多次使用await 只會 使用一個線程。
6)await/async 的缺點
1、由於編譯在搞怪、會生成大量的模板代碼、使得單個非同步方法 比 單個同步方法 運行得要慢,與之相對應的獲取到的性能優勢是、充分利用了多核心CPU,提高了任務併發量。
2、掩蓋了線程調度、使得系統開發人員無意識的忽略了該方面的性能損耗。
3、如果使用不當,容易造成死鎖
附,Demo : http://files.cnblogs.com/files/08shiyan/ParallelDemo.zip
參見更多:隨筆導讀:同步與非同步
(未完待續...)