異步編程之Async,Await和ConfigureAwait的關係

在.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新線程以後從新應用它。app

 
如下是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庫,大可沒必要兼容。
 async

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

相關文章
相關標籤/搜索