程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> .NET 實現並行的幾種方式(四),.net並行

.NET 實現並行的幾種方式(四),.net並行

編輯:關於.NET

.NET 實現並行的幾種方式(四),.net並行


本隨筆續接:.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

參見更多:隨筆導讀:同步與異步


(未完待續...)

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved