關於C#異步編程你應該瞭解的幾點建議

前段時間寫了一篇關於C#異步編程入門的文章,你能夠點擊《C#異步編程入門看這篇就夠了》查看。這篇文章咱們來討論下關於C#異步編程幾個不成文的建議,但願對你寫出高性能的異步編程代碼有所幫助。注:本文的不少內容都是學習《Effective C#》的總結。html

做者:依樂祝算法

原文地址:http://www.javashuo.com/article/p-znzrkyuf-kx.html編程

儘可能不要編寫返回值類型爲void的異步方法

在一般狀況下,建議你們不要編寫那種返回值類型爲void的異步方法,由於這樣作會破壞該方法的啓動者與方法自己之間的約定,這套約定原本能夠確保主調方可以捕獲到異步方法所發生的異常。
正常的異步方法是經過它返回的Task對象來彙報異常的。若是執行過程當中發生了異常,那麼Task對象就進入了faulted(故障)狀態。主調方在對異步方法所返回的Task對象作await操做時,該對象若已處在faulted狀態,系統則會將執行異步方法的過程當中所發生的異常拋出,反之,若Task還沒有執行到拋出異常的那個地方,則主調方的執行進度會暫停在await語句這裏,等系統稍後安排某個線程繼續執行該語句下方的那些代碼時,異常纔會拋出。c#

總結一句話就是:void的異步方法發生異常時,開發者得不到任何通知,程序既不會觸發普通的異常處理程序,也不會把這些異常記錄下來。總之,這會讓相關的線程默默的終止掉。緩存

不要把同步方法與異步方法組合起來使用

async關鍵字來修飾的方法意味着該方法有可能會在執行完全部工做以前就把控制權返回給主調方,並且,它返回給主調方的是個表明工做進度的Task對象。主調方能夠查詢此對象的狀態,以瞭解該工做是否已經完成、還沒有完成仍是在執行過程當中發生了故障。此外,這種方法還在暗示主調方:本方法所執行的工做可能要花費很長時間,所以建議你先去作其餘一些事情,稍後再來向我索要結果。
與此相反,若是把某個方法設計成同步方法,那麼意味着當該方法執行完畢時,它的後置條件一定可以獲得知足。不管這個方法要花多長時間去完成工做,它都會採用與主調方相同的資源來完成,主調方必須等這個方法完全執行完畢才能向下執行。
這兩種方法單獨寫起來都很清晰,可是若是把他們組合在一塊兒就會讓方法變得十分難用,並且有可能致使各類bug,如死鎖。所以,這裏提出兩條重要的原則。第一,不要讓同步方法必須等待異步方法執行完畢才能往下執行(儘可能不用Wait()以及.result這些阻塞式的方法)。第二,不要讓異步方法把雖然耗時很長、計算量很大可是徹底能夠由本身執行的工做轉交給另外一個異步任務去作。’
固然對於第二點,這並非說計算量較大的任務絕對不能放在單獨的線程中執行,而是說不該該把只用一個線程就能迅速作好的任務刻意的拆解成許多個較小的部分,並把他們分別放在多個新的線程上執行,而是應該把整個任務都交給某個線程來執行纔對。安全

使用異步方法時應儘可能避免線程分配

異步任務看上去好像很神奇,由於這種任務刻意轉移到另外一個地方去作,使得開啓這項任務的異步方法能夠在該任務完成以後,從早前暫停的地方繼續往下推動。不過,要想發揮異步任務的功效,就必須保證把這項任務交出去確實可以少佔用一些資源,而不是僅僅會在類似的資源之間進行上下文切換。
如:對於一個控制檯程序,若是隻是執行一項計算量較大且耗時較長的任務(或者說,運行時間較長的CPU密集型的任務),那麼把該任務單獨放在另外一個線程中並無多大好處。由於這樣作只能讓工做線程始終處於繁忙狀態,而主線程則必須一直卡在那裏等待工做線程把任務作完。在這種狀況下,其實是用兩個線程來完成本來只須要一個線程就能作好的工做,形成了資源的浪費。異步

避免沒必要要的上下文切換

目前C#代碼中使用async以及await實現的異步方法默認是把await以後的代碼放在早前捕獲的那個上下文中執行的,這是由於這樣作比較穩妥,它最多隻會引起幾回無謂的上下文切換,而不會使程序出現重大的錯誤,與之相反,若是系統不把山下文切換回去,那麼萬一遇到的是隻能在特定的上下文中才能執行的代碼,那麼程序就有可能崩潰。所以,不管有沒有必要切換上下文,系統都會切換至早前捕獲到的那個上下文,並把await以後的語句放在那個上下文執行。
若是不想讓系統作出這樣的安排,那麼能夠調用ConfigureAwait()方法。這表示接下來的那些代碼無須放在早前捕獲的上下文中執行。例如在不少程序集中,await語句以後的那些代碼通常都與上下文無關,所以與,能夠調用Task對象的ConfigureAwait()方法告訴系統,在執行完這項任務以後,沒必要專門把await下面的代碼放在早前捕獲的上下文中運行。以下所示:async

public static async Task<XElement> ReadPacket(string url)
{
    var result=await DownloadAsync(url)
                .ConfigureAwait(false);
    return XElement.Parse(result);          
}

C#語言默認讓程序把await下面的語句都放在早前捕獲的上下文中執行,這樣作雖然較爲安全,可是會下降程序的效率。所以爲了讓用戶可以更加順暢的使用程序,咱們應該調整代碼的結構,把必須運行在特定上下文的代碼剝離出來,並儘可能考慮在await語句那裏調用ConfigureAwait(false),使得程序能夠把語句下面的代碼放在默認上下文中運行,而不是切換回早前的上下文。異步編程

經過Task對象來進行異步開發

Task(任務)是一種抽象機制,能夠用來表示某項工做,因而,就可以把該工做轉交給其餘資源去完成。Task類型以及與之相關的類與結構體提供了豐富的API,讓開發者能夠操控Task對象以及由該對象所表示的工做。此外,Task對象自身也具有一些方法與屬性,能夠用來操做本對象所表示的任務。這些Task對象能夠合起來構成一項比較大的任務,他們之間既可以按照順序執行,也可以平行的執行。
能夠經過await語句來確保某些任務之間可以按照必定的順序執行,也就是說,只有當該語句所要等待的那項工做完畢以後,語句下方的代碼纔可以執行。
總之,因爲C#提供了一套豐富的API,所以能夠寫出至關優雅的算法來處理Task對象,並對這些對象所表示的任務進行安排。對任務的用法理解的越透徹,寫出來的異步代碼越清晰。
這裏簡單說明兩個經常使用的API:函數

  1. WhenAll:會根據現有的一批任務建立出一項新的任務,只有當那批任務所有執行完畢時,這項新人物纔可以完成。對Task.WhenAll所返回的新任務進行await操做會得到一份列表,早前的那些任務的執行結果就位於該列表中。
  2. WhenAny:爲了儘早的得到某個結果,可能啓動多項任務,使得他們分別從不一樣的途徑去獲取該結果。只要其中有一項任務完成,你的目標就達成了,針對這項需求,能夠考慮使用Task.WhenAny方法,並把本身所建立的那批任務傳進去。對WhenAny方法所返回的Task對象進行await操做能夠獲取到一項任務,它指的就是這批任務中最早執行完畢的那項任務。

考慮實現任務的取消協議

異步任務的編程模型(也叫基於任務的異步編程模型)提供了標準的API,用來取消任務或者廣播任務的執行進度。雖然這些API是可選的,但若是某項任務確實可以彙報其進度,或者可以予以取消,那就能夠考慮用合適的辦法來實現這些API。

針對須要取消的任務,咱們能夠經過CanclelationTokenSource對象來進行取消操做。這種對象是一種起到中介做用的對象。該對象處在有可能發出取消請求的客戶代碼與支持取消功能的那項操做之間。

若是正在執行的任務發現客戶端想要取消該操做,那麼它就會經過ThrowIfCanclellationRequested()方法拋出TaskCanclledException異常,庸醫表示整個工做流程沒有可以徹底獲得執行。

此外,返回值類型爲void類型的異步方法不該該支持取消功能。

緩存泛型異步方法的返回值

可能你在進行異步編程的時候對異步方法設置的返回類型都是Task或者Task<T>,然而有些時候把返回值類型設爲Task可能會影響性能。若是某個循環或某段代碼須要頻繁的運行,那麼系統就有可能分配不少個Task對象,從而佔用至關多的資源。好在C#提供了一種新的類型,叫作ValueTask<T>對象,他用起來比普通的Task更爲高效。該類型是值類型,所以建立這種類型的對象時,不須要再分配額外的空間。這個好處使得咱們能夠多建立一些這樣的對象,而不用擔憂它會像Task對象那樣佔據過多的資源。若是你的異步方法能夠根據早前緩存起來的結果直接返回相應的值,那麼尤爲應該考慮把返回值類型設置爲ValueTask<T>

其次,ValueTask提供了一個可以接受Task參數的構造函數,這個構造函數會在其內部等候該Task的執行結果。

總結

今天分享的內容比較多,並且不少都比較難理解,不過確實是寫出高性能異步方法所必需要掌握的技巧。因爲時間較短,所以也沒來得及經過代碼進行講述,因此須要有必定的基礎才能看懂,不過仍是但願對您有所幫助。

相關文章
相關標籤/搜索