非同步程式設計之Async,Await和ConfigureAwait的關係

Chaunce發表於2019-07-21

在.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中要特別注意非同步程式碼在並行執行情況下引發的問題

 

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

 

相關文章