Async,Await和ConfigureAwait的關系


【轉自】https://mp.weixin.qq.com/s/h10V-FshGoaQUWFPfy-azg

 

在.NET Framework 4.5中,async / await關鍵字已添加到該版本中,簡化多線程操作,以使異步編程更易於使用。為了最大化利用資源而不掛起UI,你應該盡可能地嘗試使用異步編程。雖然async / await讓異步編程更簡單,但是有一些你可能不知道的細節和注意的地方
 

新關鍵字

微軟在.NET框架中添加了async和await關鍵字。但是,使用它們,方法的返回類型應為Task類型。(我們將在稍后討論例外情況)為了使用await關鍵字,您必須在方法定義中使用async。如果你在方法定義中放入async,你應該在主體方法的某個地方至少有一處await關鍵字,如果你缺少他,你通常會收到Visual Studio的一個警告。
以下是代碼中的示例:

1 public async Task ExecuteAsync(UpdateCarCommand request, CancellationToken token = default)
2         {
3             using (var context = _contextFactory.Create())
4             {
5                 var entity = context.Cars.FirstOrDefault(a => a.Id == request.Id);
6                 // Mapping logic
7                 await context.SaveChangesAsync(token);
8             }
9         }

 

如果要從異步方法返回某些內容,可以使用Task的泛型。像以下這樣(如果你想返回受影響的行數)

1 public async Task<int> ExecuteAsync(UpdateCarCommand request, CancellationToken token = default)
2         {
3             using (var context = _contextFactory.Create())
4             {
5                 var entity = context.Cars.FirstOrDefault(a => a.Id == request.Id);
6                 // Mapping logic
7                 return await context.SaveChangesAsync(token);
8             }
9         }

 

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)的示例:

1  public async Task<int> ExecuteAsync(UpdateCarCommand request, CancellationToken token = default)
2         {
3             using (var context = _contextFactory.Create())
4             {
5                 var entity = context.Cars.FirstOrDefault(a => a.Id == request.Id);
6                 // Mapping logic
7                 return await context.SaveChangesAsync(token).CongifureAwait(false);
8             }
9         }

 

注意事項

同步 -->異步

如果要使用async/await,需要注意一些事情。您可能遇到的最大問題是處理異步方法請求同步方法。如果你開發一個新項目,通常可以將async/await從上到下貫穿於整個方法鏈中,而不需要做太多工作。但是,如果你在外層是同步的,並且必須調用異步庫,那么就會出現一些有隱患的操作。如果一不小心,便會引發大批量的死鎖

如果有同步方法調用異步方法,則必須使用ConfigureAwait(false)。如果不這樣做,就會立即掉進死鎖陷阱。發生的情況是主線程將調用async方法,最終會阻塞這個線程,直到那個async方法完成。然而,一旦異步方法完成,它必須等待原始調用者完成后才能繼續。他們都在等待對方完成,而且永遠不會。通過在調用中使用configurewait (false), async方法將能夠在另一個線程上完成自己操作,而不關心自己的狀態機的位置,並通知原始線程它已經完成。進行這個調用的最佳實踐如下:

1 [HttpPut]
2 public IActionResult Put([FromBody]UpdateCommand command) =>
3     _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 可能在任何線程上執行,並且可能並行運行!

以下代碼為例:

 1 private HttpClient _client = new HttpClient();
 2 
 3 async Task<List<string>> GetBothAsync(string url1, string url2)
 4 {
 5     var result = new List<string>();
 6     var task1 = GetOneAsync(result, url1);
 7     var task2 = GetOneAsync(result, url2);
 8     await Task.WhenAll(task1, task2);
 9     return result;
10 }
11 
12 async Task GetOneAsync(List<string> result, string url)
13 {
14     var data = await _client.GetStringAsync(url);
15     result.Add(data);
16 }

 

它下載兩個字符串並將它們放入一個List中。此代碼在舊版ASP.NET(.NetFramework)中工作正常,由於請求處設置了await,請求上下文一次只允許一個連接.

其中result.Add(data)一次只能由一個線程執行,因為它在請求上下文中執行。

但是,這個相同的代碼在ASP.NET Core上是不安全的; 具體地說,該result.Add(data)行可以由兩個線程同時執行,而不保護共享List<string>

所以在.Netcore中要特別注意異步代碼在並行執行情況下引發的問題

 

參考:https://stackoverflow.com/questions/31186354/async-await-where-is-continuation-of-awaitable-part-of-method-performed


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM