天天看點

多線程之旅(4)_async/await的用法精細詳解

GitHub源碼位址:稍後

async/await是個常見但不常用的方法。常見是因為在比較官方的代碼。片段裡經常見到這樣的搭配,不常用是因為作為開發人員來說,我們常常有更熟知的方法去代替他。

async/await到底有什麼用呢,其實網上也很少有說的明白的文章,下面我來盡量簡單明了的解釋一下。

1.使用場景

async/await這兩個關鍵字用線上程同步/異步的場景中。

2.文法方法

async和await是一種搭配用法,可以了解為兩者一般會同時出現。在寫一個方法時,async寫在定義方法的地方,await則寫在定義的方法内部。如下所示:

async Task<int> f() {
    var value = await Task.Run(()=>gosleep())
    return value.Reslut;
}

public static int gosleep()
{
    Thread.Sleep(1000);
        return 1;
}
           

當然你也可以隻寫async而不寫await,程式會給你一些警告,建議你不要這樣做,因為這種做法沒有任何意義,這種做法會導緻你不得不去阻塞主程序。具體原因後面會講

3.功能作用

async/await這兩個關鍵詞第一個很重要的用處,是說明作用,或者說可以增加代碼的可讀性。第二個作用就是表明哪些代碼要進行同步處理

當定義函數出現async時,說明這個函數中有異步的功能。這個函數要異步執行。

當函數内部出現await關鍵字時,說明在await後面跟的方法就是一個異步的方法。

4.使用async/await的特性和優點

await這個關鍵字後面表示啟動了一個線程,那問題來了,我們用Thread不行嗎,用普通的Task不行嗎?

首先,《C#并發程式設計經典執行個體》這本書上說過,編寫多線程的時候,當你開始寫new Thread時,你就已經輸了。Task是現在更好的線程處理方式,而Thread已經開始過時了。

其次,Thread一旦釋放出去,基本就無法掌握其運作狀态。比如:這個Thread線程所處理的工作結束了沒有?它應該生成的資料生成完沒有?我主程序是不是已經可以使用這個線程生成的資料了?如果線程還沒有結束,主程序還要等待多久?

當然我們可以通過一些投機取巧的方法解決這個問題,比如設定一些标記,當線程運作完成時,修改這個标記的值,主程序如果想要使用線程所生成的資料,就去循環判斷這個标記值是否被标記為完成(雖然看起來比較傻,但事實上這有可能是最安全的方法)。

如果你不想用這種很傻的,很容易被同僚和上司吐槽的方法,你就可以考慮在調用的方法中使用await這個關鍵字。

(1)使用包含await标記的線程,一般是可以擁有傳回值的,也就是線程可以用return來傳回自己所生成的資料。

(2)當主程序需要用到這個線程傳回的資料時,如果線程已經執行完成,主程序可以直接擷取線程生成的資料;

(3)如果這個線程還沒有執行完,當主程序試圖擷取線程的傳回值的時候,就會開始等待,不再繼續執行。也就是主程序會阻塞等待,進而避免拿到錯誤的資料。

(4)直到線程處理完成後,主程序成功讀取到線程return的資料,然後繼續執行後續的代碼。

最後,總的來說,如果你的程式:

(1)又想啟用多線程模式,

(2)主程序裡後續又有代碼必須依賴前面異步線程的計算結果,

(3)又不能很好的控制這兩者之間的時長間隔,

(4)又想盡量減少主程序的阻塞,

那麼就來用async/await吧。而且一旦你開始使用await後,你就會發現Task原來有一堆友善的線程同步方法可以一起使用,比如WaitAll,WhenAll,ContinueWith,Delay,GetAwaiter等等一套線程同步全家桶,用起來非常友善。

5.async/await的使用注意事項

最後在描述一遍async/await的用法,當一個被調用的方法中有線程時,這個方法要用關鍵字async修飾,這個方法中關于線程調用的代碼前,要用await關鍵字修飾。代碼見第二部分給出的示例。

當主程序走到代碼内部時,如果發現有await關鍵字,就知道此處有線程要異步執行,主程序就會自動執行後續的其他代碼,直到遇到需要使用此線程傳回值得地方,主程序才會考慮是否要停下來等待。這樣就解決了主程序阻塞的問題。

需要注意的地方是什麼呢?

async/await一定要一起使用,否則會失去意義。隻使用async一個關鍵字時,再傳回結果的時候就會報錯,不能傳回一個Task<int>類型結果如下圖:

多線程之旅(4)_async/await的用法精細詳解

程式會說報錯:

多線程之旅(4)_async/await的用法精細詳解

是以你隻能強行改變傳回值類型,最後修改成如下形式:

多線程之旅(4)_async/await的用法精細詳解

ok,你在傳回資料的時候,改成了return result.Result這種傳回值,此時程式不再報錯。但事實上此時的程式已經不再是異步的了。

原因很簡單:return result.Result中的Result,其實是var result = Task.Run(() => gosleep20())這行代碼裡傳回的資料結果;如果要擷取result.Result,也就說明程式要在return result.Result這行代碼上阻塞主程序,換句話說是主程序會被困在這個假異步方法中出不去;直到var result = Task.Run(() => gosleep20())這行代碼執行完成,才能擷取到result.Result的資料,同時放開阻塞,主程序才能繼續運作。

是以這個異步也就變得沒有意義了。

6.async/await的資料流轉圖

下面這個圖

多線程之旅(4)_async/await的用法精細詳解

關系圖中的數值對應于以下步驟。

1.事件處理程式調用并等待 AccessTheWebAsync 異步方法。

2.AccessTheWebAsync 可建立 HttpClient 執行個體并調用 GetStringAsync 異步方法以下載下傳網站内容作為字元串。

3.GetStringAsync 中發生了某種情況,該情況挂起了它的程序。 可能必須等待網站下載下傳或一些其他阻止活動。 為避免阻止資源,GetStringAsync 會将控制權出讓給其調用方 AccessTheWebAsync。

GetStringAsync 傳回 Task,其中 TResult 為字元串,并且 AccessTheWebAsync 将任務配置設定給 getStringTask 變量。 該任務表示調用 GetStringAsync 的正在進行的程序,其中承諾當工作完成時産生實際字元串值。

4.由于尚未等待 getStringTask,是以,AccessTheWebAsync 可以繼續執行不依賴于 GetStringAsync 得出的最終結果的其他工作。 該任務由對同步方法 DoIndependentWork 的調用表示。

5.DoIndependentWork 是完成其工作并傳回其調用方的同步方法。

6.AccessTheWebAsync 已用完工作,可以不受 getStringTask 的結果影響。 接下來,AccessTheWebAsync 需要計算并傳回該下載下傳字元串的長度,但該方法僅在具有字元串時才能計算該值。

是以,AccessTheWebAsync 使用一個 await 運算符來挂起其進度,并把控制權交給調用 AccessTheWebAsync 的方法。 AccessTheWebAsync 将 Task(Of Integer) 或 Task<int> 傳回至調用方。 該任務表示對産生下載下傳字元串長度的整數結果的一個承諾。

 備注

如果 GetStringAsync(是以 getStringTask)在 AccessTheWebAsync 等待前完成,則控件會保留在 AccessTheWebAsync 中。如果異步調用過程 (getStringTask) 已完成,并且 AccessTheWebSync 不必等待最終結果,則挂起然後傳回到 AccessTheWebAsync 将造成成本浪費。

在調用方内部(此示例中的事件處理程式),處理模式将繼續。 在等待結果前,調用方可以開展不依賴于 AccessTheWebAsync 結果的其他工作,否則就需等待片刻。事件處理程式等待 AccessTheWebAsync,而 AccessTheWebAsync 等待 GetStringAsync。

7.GetStringAsync 完成并生成一個字元串結果。 字元串結果不是通過按你預期的方式調用 GetStringAsync 所傳回的。(請記住,此方法已在步驟 3 中傳回一個任務。)相反,字元串結果存儲在表示完成方法 getStringTask 的任務中。 await 運算符從 getStringTask 中檢索結果。 指派語句将檢索到的結果賦給 urlContents。

8.當 AccessTheWebAsync 具有字元串結果時,該方法可以計算字元串長度。 然後,AccessTheWebAsync 工作也将完成,并且等待事件處理程式可繼續使用。 在此主題結尾處的完整示例中,可确認事件處理程式檢索并列印長度結果的值。

如果你不熟悉異步程式設計,請花 1 分鐘時間考慮同步行為和異步行為之間的差異。 當其工作完成時(第 5 步)會傳回一個同步方法,但當其工作挂起時(第 3 步和第 6 步),異步方法會傳回一個任務值。 在異步方法最終完成其工作時,任務會标記為已完成,而結果(如果有)将存儲在任務中。

7.總結

好了,搞了這麼老半天終于說完了

附上兩個很厲害的連結:

https://docs.microsoft.com/zh-cn/previous-versions/hh191443(v=vs.120)

https://docs.microsoft.com/zh-cn/previous-versions/hh873191%28v%3dvs.120%29

測試代碼見文首GitHub連結

繼續閱讀