本隨筆續接:.NET 實現並行的幾種方式(三)
前三篇隨筆已經介紹了多種方式、利用多線程、充分利用多核心CPU以提高運行效率。但是以前的方式在WebAPI和GUI系統上、
使用起來還是有些繁瑣,尤其是在需要上下文的情況下。而await/async就是在這樣的情況下應運而生,並且它可以在理論上讓CPU跑到100%。
async修飾符:它用以修飾方法、lambda表達式、匿名方法,以標記方法為異步方法。異步方法必須遵循的規范如下:
1、返回值僅切僅有三種: void、Task、Task<T>.
2、方法參數不可以使用 ref、out類型參數。
await運算符:它用以標記一個系統可在其上恢復執行的掛起點。該運算符會告訴computer不會再往下繼續執行該方法、直到等待的異步方法執行完畢為止。
同時會將程序的控制權return給其調用者。await表達式不阻止正在執行它的線程。 而是讓編譯器將異步方法剩余部分注冊為等待任務的延續任務。 當等待任務完成時,它會調用其延續任務,如同在掛起點上恢復執行。
該圖出自: https://msdn.microsoft.com/zh-cn/library/mt674882.aspx
原因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類型。
使用混合編程的死鎖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
上文也說過了,當異步任務完成後、它會嘗試在之前的上下文環境中恢復執行。這樣帶來的問題是時間片會被切分成更多、造成更多的線程調度上的性能損耗。
一旦時間片被切分的過多、尤其是在GUI和Web具有上下文環境中運行,影響會更大。
另外,使用ConfigureAwait忽略上下文後、可避免死鎖。 因為當等待完成時,它會嘗試在線程池上下文中執行 async 方法的剩余部分,不會存在線程等待。
使用一個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 只會 使用一個線程。
1、由於編譯在搞怪、會生成大量的模板代碼、使得單個異步方法 比 單個同步方法 運行得要慢,與之相對應的獲取到的性能優勢是、充分利用了多核心CPU,
提高了任務並發量。
2、增加了掩蓋掉了線程調度、使得系統開發人員無意識的忽略了該方面的性能損耗。
3、如果使用不當,容易造成死鎖
附,Demo : http://files.cnblogs.com/files/08shiyan/ParallelDemo.zip
參見更多:隨筆導讀:同步與異步
(未完待續...)