2019年1月15日 星期二

C#有關async/await的實作方式

先來看一下.NET Doc的說明

" C# 5 引進了一種簡化的方法 (非同步程式設計)

來運用 .NET Framework 4.5 和更新版本

.NET Core 以及 Windows 執行階段中的非同步支援 "



而async/await主要是有關I/O bound的非同步處理

C#本身提供的API也加入了許多支援非同步處理的方法:


而使用async/await撰寫非同步處理會變得相對容易

但一般網路上找不太到前後的比對

到底是變得多容易?而中間的差異到底是什麼?

而必須再強調一點

async/await是用來處理有關I/O bound的非同步作業

先看看原本的做法如下:


在Main裡面一開始先啟動stopwatch計時器

接著呼叫一個HttpGetStringLength的方法

裡面就用正常同步的方式去呼叫GetStringAsync().Result

然後把字串長度印出來

這邊在印Console時另外實作一個ConsoleMessage的方法

順帶把正在執行的thread的id與程式執行時間印出來

得到結果如下:


這個結果很正常,就是一個執行緒跑到完

如果把HttpGetStringLength改成非同步的做法


HttpGetStringLengthAsync方法裡面在HttpClinet.GetStringAsync後

採用await的方式,非同步等待

但由於HttpGetStringLengthAsync這個方法是async Task

所以VS2017會有提示訊息


告訴你說,Main在呼叫到HttpGetStringLengthAsync這個方法時

不會等候此呼叫,會直接往下執行

那接著看印出的結果如下:


這個結果在一般的多執行緒的概念來說蠻怪的

可能很多人會以為非同步就會是HttpGetStringLengthAsync這個方法都是另外的執行緒在執行

但thread 1一直執行到印出HttpGetStringLengthAsync in

之後會回到Main function繼續往下印出Main back

而在HttpGetStringLengthAsync方法的await getStringTask後

就是另外一個執行緒thread 9在執行


這奇怪的地方在於,執行緒的斷點會在方法之中!!

這如果要用過去的方式來理解,第一個想到的方式就是callback機制

非同步的實作方式可能會像這樣:



就是在處理http client的連線時,用一個Func<string>委派function來執行

以及實作一個AsyncCallBack委派asyncCallback
(實作callback方法OnWorkCallBack引數為IAsyncResult)

並且採用BeginInvoke的方式非同步呼叫,並帶入委派實體:asyncCallback, function

OnWorkCallBack方法中先取出傳入的委派function

再利用EndInvoke取出function的結果

其印出的結果如下:


多印了一個在Func<string>裡面的資訊

可以清楚看到從Func<string>裡面開始就是由thread 3處理

一直到callback方法OnWorkCallback都是thread 3

後來有了Task.Run非同步作業機制,也可以寫成如下方式


類似callback的方式來實作後續要做的事情

印出的結果如下:


目前提了兩種callback的非同步實作方式

只是方便理解async/await的執行緒運作"流程"

過去可能會以為這樣運作的非同步效果是一樣的

但實際上在最後httpClient.GetStringAsync的運作上還是有所不同

string getString = httpClient.GetStringAsync("https://google.com.tw").Result;

以及

string getString = await httpClient.GetStringAsync("https://google.com.tw");

這兩行程式碼的運作機制是不一樣的!


async/await要解決的主要是I/O bound的非同步

而非兩個不同cpu bound的非同步

像第一個單執行緒的範例一樣


由於httpClient.GetStringAsync為I/O動作,速度較慢

這個thread 1會持續等待httpClient.GetStringAsync的回傳值

這個等待的過程中,thread 1是被占用而浪費的

而C#在提供Async結尾的方法時

就是進行I/O處理的優化,並且在I/O處理時不佔用執行緒

就是第二種採用await的方式


在await後面直接印出getString.Length是thread 9

其乍看之下好像流程跟後來用callback實作的方式很像


無論是BeginInvoke或是Task.Run,在task in跟task getString都還是thread 3 !!


其中關鍵的點就是在await這行程式

採用await方式去呼叫Async的方法

其所謂的非同步的意思應該是:

原本執行的thread不等待I/O,同時間I/O開始進行工作

I/O工作結束後,會再找一個新的thread來接續await後面的工作

這之間不同的點如下圖:

第一種,在執行到await後,I/O開始作業

而thread 1會回到Main繼續往下執行

而用BeginInvoke或Task.ContinueWith,則是產生一個新的thread去執行I/O

thread 1回到Main繼續往下執行


在C#後來所提供有Async結尾的方法,如果你採用await的方式

執行緒的利用會如上圖的第一條長條圖,中間的I/O處理,其實是不佔用執行緒的

在I/O這段時間內,整個程式還是只有thread 1在執行

而透過BeginInvoke或Task.Run的方式,雖然也達成了非同步處理

但由於在I/O這段非採用await的方式,則會讓產生的新thread去等待I/O完成

在I/O這段時間內,整個程式有thread 1持續執行,而thread 3在等待中

所以這段時間,thread 3的執行效率其實是浪費掉的


這對有大量I/O動作(資料庫存取、檔案讀寫、網路傳輸)的程式來說

C#在底層提供這樣的非同步I/O機制

能大幅減低CPU執行緒的占用,避免不必要的等待浪費

這也是有些人會認為async/await是C#在導入lambda語法後最重要的功能了!!


以上是針對非Web API、Windows Form、WPF的運作狀況做討論

上述的thread流程又會稍微不同 XDD

但整體而言

async/await為非同步I/O作業

這樣的稱呼可能比較容易理解

而其不占用thread的方式,也可以解決Web API或windows UI被lock的問題


以上做點紀錄幫自己釐清有關async/await的運作


6 則留言: