訂閱編程
原文地址:https://cpratt.co/async-tips-tricks/安全
Task.Run(() => DoSyncStuff());
從技術上講,這是假的異步。它仍然阻塞,但它運行在後臺線程上。這對於防止使用桌面/移動應用程序阻止UI線程很是有用。在Web應用程序上下文中,這幾乎沒有意義,由於每一個線程都來自同一個池,用於處理主(請求)線程來自的請求,而且在完成全部操做以前不會返回響應。app
對於這個,咱們有一個從微軟借來的助手類。它看起來是各類命名空間,但老是做爲內部命名空間,所以您不能直接從框架中使用它。框架
public static class AsyncHelper
{
private static readonly TaskFactory _taskFactory = new
TaskFactory(CancellationToken.None,
TaskCreationOptions.None,
TaskContinuationOptions.None,
TaskScheduler.Default);
public static TResult RunSync<TResult>(Func<Task<TResult>> func) => _taskFactory .StartNew(func) .Unwrap() .GetAwaiter() .GetResult(); public static void RunSync(Func<Task> func) => _taskFactory .StartNew(func) .Unwrap() .GetAwaiter() .GetResult(); }
而後異步
AsyncHelper.RunSync(() => DoAsyncStuff());
當您await
進行異步操做時,默認狀況下會傳遞調用代碼的上下文。這可能會對性能產生不小的影響。若是您之後不須要恢復該上下文,那麼它只是浪費了資源。您能夠經過附加ConfigureAwait(false)
到您的通話來阻止此行爲:async
await DoSomethingAsync().ConfigureAwait(false);
你應該老是這樣作,除非有特定的理由保持上下文。某些狀況包括您須要訪問特定GUI組件或須要從控制器操做返回響應時。異步編程
但重要的是,每一個操做都有本身的上下文,所以您能夠安全地ConfigureAwait(false)
在須要維護上下文的代碼調用的異步方法中使用; 你只是沒法使用ConfigureAwait(false)
該方法自己。例如:post
public async Task<IActionResult> Foo() { // No `ConfigureAwait(false)` here await DoSomethingAsync(); return View(); } ... public async Task DoSomethingAsync() { // This is fine await DoSomethingElseAsync().ConfigureAwait(false); }
所以,您能夠而且應該將須要維護上下文的多個異步操做分解爲單獨的方法,所以您只須要保留上下文一次,而不是N次。例如:性能
public async Task<IActionResult> Foo() { await DoFirstThingAsync(); await DoSecondThingAsync(); await DoThirdThingAsync(); return View(); }
在這裏,每一個操做都得到調用代碼的上下文的副本,而且因爲咱們須要該上下文,所以使用ConfigureAwait(false)
不是一個選項。可是,經過重構如下代碼,咱們只須要調用代碼的上下文的單個副本。google
public async Task DoThingsAsync()
{
await DoFirstThingAsync().ConfigureAwait(false);
await DoSecondThingAsync().ConfigureAwait(false);
await DoThirdThingAsync().ConfigureAwait(false);
}
public async Task<IActionResult> Foo() { await DoThingsAsync(); return View(); }
在同步代碼中,局部變量進入堆棧並在超出範圍時被丟棄。可是,因爲在等待異步操做時發生上下文切換,所以必須保留這些局部變量。框架經過將它們添加到堆上的結構來實現此目的。這樣,當執行返回到調用代碼時,能夠恢復本地。可是,在代碼中進行的操做越多,就必須將更多內容添加到堆中,從而致使更頻繁的GC循環。其中一些多是不可避免的,可是當您要等待異步操做時,您應該注意無用的變量賦值。例如,代碼如:
var today = DateTime.Today; var todayString = today.ToString("MMMM d, yyyy");
這將致使兩個不一樣的值進入堆,而若是您只須要todayString
,只需將代碼重寫爲:
var todayString = DateTime.Today.ToString("MMMM d, yyyy");
除非有人告訴你,這是你沒有想到的事情之一。
C#中異步的一個好處是能夠取消任務。若是用戶在UI中取消任務,導航離開網頁等,這容許您停止任務。要啓用取消,您的異步方法應接受CancellationToken
參數。
public async Task DoSomethingAsync(CancellationToken cancellationToken) { ... }
而後,該取消令牌應該傳遞給該方法調用的任何其餘異步操做。若是能夠而且但願取消,則該方法的責任是啓用取消。並不是全部異步任務均可以取消。通常來講,是否能夠取消任務取決於該方法是否具備接受的重載CancellationToken
。
在某些狀況下,若是方法未提供接受的重載,您仍能夠取消任務CancellationToken
。你並無真正取消這項任務,可是根據實施狀況,你可能會停止它,但仍然能夠有效地得到相同的結果。例如,該ReadAsStringAsync
方法HttpContent
沒有接受的重載CancellationToken
。可是,若是您丟棄了HttpResponseMessage
,則會停止讀取內容的嘗試。
try { using (var response = await httpClient.GetAsync(new Uri("https://www.google.com"))) using (cancellationToken.Register(response.Dispose)) { return await response.Content.ReadAsStringAsync(); } } catch (ObjectDisposedException) { if (cancellationToken.IsCancellationRequested) throw new OperationCanceledException(); throw; }
從本質上講,咱們使用CancellationToken
調用Dispose
的HttpResponseMessage
狀況下,若是它取消。這將致使ReadAsStringAsync
拋出一個ObjectDisposedException
。咱們捕獲了這個異常,若是CancellationToken
已經取消,咱們會拋出異常OperationCanceledException
。
這種方法的關鍵在於可以處理某些父對象,這會致使沒法取消的方法引起異常。它不適用於全部內容,但能夠在某些狀況下爲您提供幫助。
async
/ await
關鍵字可使用如下任一方法編寫異步方法:
public async Task FooAsync() { await DoSomethingAsync(); } public Task BarAsync() { return DoSomethingAsync(); }
首先,在方法中等待異步操做,而後在返回到調用代碼以前將結果包裝在另外一個任務中。在第二步中,直接返回異步操做的任務。若是你有一個只調用另外一個異步方法的異步方法(一般是異步重載的狀況),那麼你應該忽略async
/ await
keywords,就像上面的第二種方法同樣。
使用async
關鍵字的方法能夠安全地拋出異常。編譯器將負責將異常包裝在一個Task
。
public async Task FooAsync() { // This is fine throw new Exception("All your bases are belong to us."); }
可是,Task
沒有async
關鍵字的返回方法應該返回一個Task
例外。
public Task FooAsync() { try { // Code that throws exception } catch (Exception e) { return Task.FromException(e); } }
一般在開發方法的同步和異步版本時,您會發現兩個實現之間惟一真正的區別是,一個調用各類方法的異步版本,而另外一個調用同步版本。當實現幾乎相同時,除了使用async / await以外,您能夠利用各類「黑客」來分解重複的代碼。我發現的最好和最少「hacky」方法被稱爲「Flag Argument Hack」。本質上,您引入了一個布爾值,指示該方法是應該使用同步仍是異步訪問,而後相應地進行分支:
private async Task<string> GetStringCoreAsync(bool sync, CancellationToken cancellationToken) { return sync ? SomeLibrary.GetString() : await SomeLibrary.GetStringAsync(cancellationToken).ConfigureAwait(false); } public string GetString() => GetStringCoreAsync(true, CancellationToken.None) .ConfigureAwait(false) .GetAwaiter() .GetResult(); public Task<string> GetStringAsync() => GetStringAsync(CancellationToken.None); public Task<string> GetStringAsync(CancellationToken cancellationToken) => GetStringCoreAsync(false, cancellationToken);
這彷佛是不少代碼,因此讓咱們解開它。首先,咱們有一個私人方法GetStringCoreAsync
。這是咱們分解公共代碼的地方。在這裏,咱們只是調用其餘一些具備同步和異步方法的庫來獲取某種字符串。不能否認,對於這種簡單化的東西,你真的不該該使用這個hack,而應該只是讓每一個方法直接調用它的相應對應物。可是,我不想經過引入過於複雜的實現來阻礙理解。正如您所看到的,這裏的要點是咱們正在分支sync
使用庫中的同步或異步方法的值。只要您等待異步方法,這將正常工做,這意味着此私有方法須要具備async
關鍵字。咱們'CancellationToken
若是內部使用的異步方法是可取消的。
接下來,咱們只有調用私有方法的同步和異步實現。對於同步版本,咱們須要Task
從私有方法中解包返回的內容。爲此,咱們使用該GetAwaiter().GetResult()
模式安全地阻止異步調用。這裏沒有死鎖的危險,由於雖然私有方法是異步的,可是當咱們傳遞true
時sync
,實際上並無使用異步方法。咱們還ConfigureAwait(false)
用來防止附加同步上下文,由於它徹底沒有必要膨脹:這裏沒有線程切換的可能性。
異步實現至關不起眼。CancellationToken.None
若是沒有傳遞取消令牌,則會有一個超時傳遞默認值,而後實際實現只是false
爲sync
參數調用私有方法幷包含取消令牌。
有一種思想流派認爲方法不該該像這樣的布爾分支。若是您有兩組獨立的邏輯,那麼您應該有兩個單獨的方法。這有一些道理,但我認爲必須權衡邏輯實際上有多麼不一樣。所以,若是您有大量重複的邏輯,這是分解公共代碼的好方法。可是,它應該是這方面的最後手段。若是代碼的某些部分是CPU綁定的或以其餘方式同步運行,那麼您應該首先嚐試將這些代碼部分分解出來。你的同步和異步方法之間可能仍然存在一些重複,可是若是你能夠將大部份內容都放到可使用的方法而不訴諸黑客,那麼這就是最佳路徑。
還有一個論點要說,若是你有那麼多的邏輯,你的方法可能首先作得太多了。你必須讓本身的判斷規則。有時候作這樣的事情其實是最好的路徑,可是在使用這種方法以前你應該仔細評估是不是這種狀況。
class Program { static void Main(string[] args) { MainAsync(args).GetAwaiter().GetResult(); } static async Task MainAsync(string[] args) { // await something } }
對於它的價值,C#7.1承諾Async Main支持,因此你只需:
class Program { static async Task Main(string[] args) { // await something } }
可是,在撰寫本文時,這不起做用。不過,這真的只是語法糖。當編譯器遇到異步Main時,它只是將它包裝在常規同步Main中,就像在第一個代碼示例中同樣。
有不少術語與C#中的異步混淆。您據說同步代碼會阻塞該線程,而異步代碼則不會。這實際上不是真的。不管線程是否被阻止,實際上與同步仍是異步都沒有任何關係。它進入討論的惟一緣由是,若是你的目標是不阻塞線程,async至少比同步更好,由於有時候,在某些狀況下,它可能只是在不一樣的線程上運行。若是您的異步方法中有任何同步代碼(任何不等待其餘內容的代碼),那麼代碼將始終運行同步。此外,若是等待的內容已經完成,則異步操做能夠運行同步。最後,async不能確保工做不會在同一個線程上實際完成。它只是爲線程切換開闢了可能性。
若是你須要確保異步操做不會阻塞線程,例如對於你想要保持GUI線程打開的桌面或移動應用程序,那麼你應該使用:
Task.Run(() => DoStuffAsync());
等待。這與咱們上面用來運行同步「async」不同嗎?是的。一樣的原則適用:Task.Run
將運行您在新線程上傳遞給它的委託。反過來,這意味着它不會在當前線程上運行。
Task
兼容大多數異步方法返回Task
,但並不是全部Task
返回方法都必須是異步的。這可能有點使人費解。比方說,你須要實現一個返回的方法Task
或,但你實際上並不有什麼異步作。Task<TResult>
public Task DoSomethingAsync(CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { return Task.FromCanceled(cancellationToken); } try { DoSomething(); return Task.FromResult(0); } catch (Exception e) { return Task.FromException(e); } }
首先,這確保了操做沒有被取消。若是有,則返回已取消的任務。而後,咱們須要作的同步工做包含在一個try..catch
塊中。若是拋出異常,咱們將須要返回一個包含該異常的錯誤任務。最後,若是它正確完成,咱們將返回一個已完成的任務。
重要的是要意識到這實際上並非異步。DoSomething
仍然是同步並將阻止。可是,如今它能夠像處理異步同樣處理,由於它返回一個任務,就像它應該的那樣。你爲何要這樣作?好吧,一個例子是在實現適配器模式時,您正在適應的其中一個源不提供異步API。您仍然必須知足接口,但您應該註釋該方法以代表它實際上不是異步。那些想要在他們不須要阻塞線程的狀況下使用這種方法的人能夠選擇經過將其做爲委託傳遞來調用它Task.Run
。
C#中異步編程的一個方面並非很明顯,即任務返回「熱門」或已經開始。該await
關鍵字用於暫停代碼,直到任務完成,但實際上並未啓動它。當您同時查看對運行任務的影響時,這會變得很是有趣。
await FooAsync(); await BarAsync(); await BazAsync();
這裏,三個任務串行運行。只有在FooAsync
完成後纔會BarAsync
啓動,一樣,BazAsync
直到BarAsync
完成纔會啓動。這是因爲正在等待內聯任務。如今,請考慮如下代碼:
var fooTask = FooAsync(); var barTask = BarAsync(); var bazTask = BazAsync(); await fooTask; await barTask; await bazTask;
在這裏,任務如今並行運行。這是由於這三個都是在全部三我的隨後等待以前開始的,由於他們又回來了。
考慮到Task.WhenAll
存在,這彷佛有點反直覺。若是全部任務都已在運行,爲何須要該功能?簡單地說,Task.WhenAll
做爲一種等待完成一組任務的方式存在,以便在全部結果都準備好以前代碼不會繼續。
var factor1Task = GetFactor1Async(); var factor2Task = GetFactor2Task(); await Tasks.WhenAll(factor1Task, factor2Task); var value = factor1Task.Result * factor2Task.Result;
因爲兩個任務都須要在咱們運行乘法線以前完成,所以咱們能夠暫停,直到兩個任務完成等待Task.WhenAll
。不然,它並不重要。事實上,Task.WhenAll
若是您等待兩個任務而不是Result
直接調用,您甚至能夠放棄:
var value = (await factor1Task) * (await factor2Task);
不管多長時間,它真的只是一個品味問題而不是任何東西。儘管如此,重要的是要意識到任務當即開始,而不是等待它們的行爲致使它們開始。相反,等待只是阻止代碼繼續前進,直到任務完成。