在.NET Framework 4.5中,async / await關鍵字已新增到該版本中,簡化多執行緒操作,以使非同步程式設計更易於使用。為了最大化利用資源而不掛起UI,你應該儘可能地嘗試使用非同步程式設計。雖然async / await讓非同步程式設計更簡單,但是有一些你可能不知道的細節和注意的地方
新關鍵字
微軟在.NET框架中新增了async和await關鍵字。但是,使用它們,方法的返回型別應為Task型別。(我們將在稍後討論例外情況)為了使用await關鍵字,您必須在方法定義中使用async。如果你在方法定義中放入async,你應該在主體方法的某個地方至少有一處await關鍵字,如果你缺少他,你通常會收到Visual Studio的一個警告。
以下是程式碼中的示例:
public async Task ExecuteAsync(UpdateCarCommand request, CancellationToken token = default) { using (var context = _contextFactory.Create()) { var entity = context.Cars.FirstOrDefault(a => a.Id == request.Id); // Mapping logic await context.SaveChangesAsync(token); } }
如果要從非同步方法返回某些內容,可以使用Task的泛型。像以下這樣(如果你想返回受影響的行數)
public async Task<int> ExecuteAsync(UpdateCarCommand request, CancellationToken token = default) { using (var context = _contextFactory.Create()) { var entity = context.Cars.FirstOrDefault(a => a.Id == request.Id); // Mapping logic return await context.SaveChangesAsync(token); } }
async.await給我們帶來了什麼?
雖然使用這個看起來很簡單,但是它有什麼幫助呢?最後,所有這些操作都是在等待資料庫返回結果時(在本例中)讓其他請求使用當前執行緒。當您向資料庫、磁碟、internet等外部源發出可能需要一段時間才能執行的請求時,我們可以使用async/ wait讓其他請求使用這個執行緒。這樣,我們就不會有空閒的“worker”(執行緒)在那裡等待完成其他任務。這就像去快餐店一樣,在你點完菜之後,其他人不會點任何東西,直到你吃完為止。使用async/ await,其他人可以在你點完菜之後下他們的訂單,並且可以同時處理多個訂單。
它不能做什麼?
這裡需要注意的一件事是async/await並不是並行/多核程式設計。當您使用async/await時,只處理該執行緒,並讓其他執行緒使用它。程式碼的作用類似於“同步”,因為您可以在await之後以本方法繼續執行程式碼。因此,如果在一個方法中有四個await,則必須等到每個方法都完成後才能呼叫下一個方法。因此,您必須使用任務庫或任何您喜歡的方法生成新執行緒,以使它們並行執行。但是,您也可以讓每個執行緒使用async/wait,這樣它們就不會阻塞資源了!
ConfigureAwait(false)能做什麼呢?
預設情況下,當您使用async/await時,它將在開始請求的原始執行緒上繼續執行(狀態機)。但是,如果當前另一個長時間執行的程式已經接管了該執行緒,那麼你就不得不等待它完成。要避免這個問題,可以使用ConfigureAwait的方法和false引數。當你用這個方法的時候,這將告訴Task它可以在任何可用的執行緒上恢復自己繼續執行,而不是等待最初建立它的執行緒。這將加快響應速度並避免許多死鎖。
但是,這裡有一點點損失。當您在另一個執行緒上繼續時,執行緒同步上下文將丟失,因為狀態機改變。這裡最大的損失是你會失去歸屬於執行緒的Culture和Language,其中包含了國家語言時區資訊,以及來自原始執行緒的HttpContext.Current之類的資訊,因此,如果您不需要以此來做多語系或操作任何HttpContext型別設定,則可以安全地進行此方法的呼叫。注意:如果需要language/culture,可以始終在await之前儲存當前相關狀態值,然後在await新執行緒之後重新應用它。
以下是ConfigureAwait(false)的示例:
public async Task<int> ExecuteAsync(UpdateCarCommand request, CancellationToken token = default) { using (var context = _contextFactory.Create()) { var entity = context.Cars.FirstOrDefault(a => a.Id == request.Id); // Mapping logic return await context.SaveChangesAsync(token).CongifureAwait(false); } }
注意事項
同步 -->非同步
如果要使用async/await,需要注意一些事情。您可能遇到的最大問題是處理非同步方法請求同步方法。如果你開發一個新專案,通常可以將async/await從上到下貫穿於整個方法鏈中,而不需要做太多工作。但是,如果你在外層是同步的,並且必須呼叫非同步庫,那麼就會出現一些有隱患的操作。如果一不小心,便會引發大批量的死鎖
如果有同步方法呼叫非同步方法,則必須使用ConfigureAwait(false)。如果不這樣做,就會立即掉進死鎖陷阱。發生的情況是主執行緒將呼叫async方法,最終會阻塞這個執行緒,直到那個async方法完成。然而,一旦非同步方法完成,它必須等待原始呼叫者完成後才能繼續。他們都在等待對方完成,而且永遠不會。通過在呼叫中使用configurewait (false), async方法將能夠在另一個執行緒上完成自己操作,而不關心自己的狀態機的位置,並通知原始執行緒它已經完成。進行這個呼叫的最佳實踐如下:
[HttpPut] public IActionResult Put([FromBody]UpdateCommand command) => _responseMediator.ExecuteAsync(command).ConfigureAwait(false).GetAwaiter().GetResult();
.NET Standard與ConfigureAwait(false)
在.NETCore中,微軟刪除了導致我們在任何地方都需要ConfigureAwait(false)的SynchronizationContext。因此,ASP.NETCore應用程式在技術上不需要任何ConfigureAwait(false)邏輯,因為它是多餘的。但是,如果在開發有一個使用.NETStandard的庫,那麼強烈建議仍然使用.ConfigureAwait(false)。在.NETCore中,這自動是無效的。但是如果有.NETFramework的人最終使用這個庫並同步呼叫它,那麼它們將會遇到一堆麻煩。但是隨著.NET5是由.NETCore構建的,所以未來大多都是.NetCore呼叫.Netstadard,你如果不準備讓.NetFramework呼叫你的standard庫,大可不必相容。
ConfigureAwait(false) 貫穿始終
如果同步呼叫有可能呼叫您的非同步方法,那麼在整個呼叫堆疊的每個非同步呼叫上,您都將被迫設定. configureAwait (false) !如果不這樣做,就會導致另一個死鎖。這裡的問題是,每個async/ await對於呼叫它的當前方法都是本地的。因此,呼叫鏈的每個異async/await都可能最終在不同的執行緒上恢復。如果一個同步呼叫一路向下,遇到一個沒有configurewait(false)的任務,那麼這個任務將嘗試等待頂部的原始執行緒完成,然後才能繼續。雖然這最終會讓你感到心累,因為要檢查所有呼叫是否設定此屬性。
開銷
雖然async/ await可以極大地增加應用程式一次處理的請求數量,但是使用它是有代價的。每個async/ await呼叫最終都將建立一個小狀態機來跟蹤所有資訊。雖然這個開銷很小,但是如果濫用async/ await,則會導致速度變慢。只有當執行緒不得不等待結果時,才應該等待它。
Async Void
雖然幾乎所有的async / await方法都應返回某種型別的Task,但此規則有一個例外:有時,您可以使用async void。但是,當您使用它時,呼叫者實際上不會等待該任務完成後才能恢復自己。它實際上是一種即發即忘的東西。有兩種情況你想要使用它。
第一種情況是事件處理程式,如WPF或WinForms中的按鈕單擊。預設情況下,事件處理程式的定義必須為void。如果你把一個任務放在那裡,程式將無法編譯,並且返回某些東西的事件會感覺很奇怪。如果該按鈕呼叫非同步async,則必須執行async void才能使其正常工作。幸運的是,這是我們想要的,因為這種使用不會阻塞UI。
第二個是請求你不介意等待獲得結果的東西。最常見的示例是傳送日誌郵件,但不想等待它完成或者不關心它是否完成。
然而,對於這兩種情況,都有一些缺點。首先,呼叫方法不能try/catch呼叫中的任何異常。它最終將進入AppDomain UnhandledException事件。不過,如果在實際的async void方法中放入一個try catch,就可以有效地防止這種情況發生。另一個問題是呼叫者永遠不會知道它何時結束,因為它不返回任何東西。因此,如果你關心什麼時候完成某個Task,那麼實際上需要返回一個Task。
探討.NetCore中非同步注意事項
在.NetCore中已經剔除了SynchronizationContext,剔除他的主要原因主要是效能和進一步簡化操作
在.NetCore中我們不用繼續關心非同步同步混用情況下,是否哪裡沒有設定ConfigureAwait(false) 會導致的死鎖問題,因為在.netcore中的async/await 可能在任何執行緒上執行,並且可能並行執行!
以下程式碼為例:
private HttpClient _client = new HttpClient(); async Task<List<string>> GetBothAsync(string url1, string url2) { var result = new List<string>(); var task1 = GetOneAsync(result, url1); var task2 = GetOneAsync(result, url2); await Task.WhenAll(task1, task2); return result; } async Task GetOneAsync(List<string> result, string url) { var data = await _client.GetStringAsync(url); result.Add(data); }
它下載兩個字串並將它們放入一個List中。此程式碼在舊版ASP.NET(.NetFramework)中工作正常,由於請求處設定了await,請求上下文一次只允許一個連線.
其中result.Add(data)
一次只能由一個執行緒執行,因為它在請求上下文中執行。
但是,這個相同的程式碼在ASP.NET Core上是不安全的; 具體地說,該result.Add(data)
行可以由兩個執行緒同時執行,而不保護共享List<string>
。
所以在.Netcore中要特別注意非同步程式碼在並行執行情況下引發的問題