[C#]Multiple thread和非同步的差異,並正確自訂非同步的方式
前言
有時候因為工作遇到了效能的問題,需要使用thread或async的方法來處理時,總是常常會聽到工程師對非同步和Task或thread的運作原理不清楚,所以搞不清楚差異性,於是就比較深入的了解這個議題,並且筆記下來方便還不了解或觀念有誤解的工程師去閱讀。
Thread vs 非同步的比較
為何需要有multiple thread和非同步呢?就是為了要加快我們程式的回應,這部份不管是在win form的ui,或者是web api的回應上,都可以使用相對應的方式來改進效能和回應速度,我們先了解一台電腦通常會有cpu和記憶體跟硬碟讀取,想要讓原本效能不佳的問題得到解決,無非就是把空閒的主機裝置,加以利用來加快程式的回應速度,而常常看到的thread或task.run和parallel都是提高cpu的使用率來讓程式碼達成並行的效果,而微軟自訂的httpclient或stream裡面帶有async結尾的方法,則是使用I/O的方式來達成並行,而這種有async結尾的方法名稱,微軟就定義為非同步方法。
而何謂並行呢?並行跟非同步都是為了達成讓程式不阻塞,只是因為.net是可以充份使用thread的framework,所以就把I/O達成並行的方式另稱為非同步,像client端的javascript就是single thread概念的,所以任何可以不阻塞的方式在javascript一律就稱為非同步。
接著就得談談何時該選擇使用thread,何時則使用I/O的方式呢?其實只要是使用到微軟或一些第三方library定義的async方法,就完全不要再去考慮使用thread了,每台主機的thread是有限制數量的,每次從使用方訪問一個request的時候,就會從thread pool拉一條執行緒來服務,直到response的時候才會釋放此thread回到pool裡服務其他用戶,但每台主機能提供的thread是有限的,我們有理由相信微軟提供的async方法,是不會另起新的thread來達成工作,所以我們即可以達到多工切換又可以保証thread不會卡住,截取MSDN上的一段內容"
硬體上的理論
I/O需要的CPU資源非常的少,大部份工作是交由DMA(Direct Memory Access)完成的,而CPU是不會直接跟硬碟溝通的,他們之間會透過DMA做溝通,當非同步完成之後,DMA會通知CPU任務完成,拿回非同步方法的結果,把一個任務完成,所以簡單來說如果我們想要讓硬體發揮最好的效能,又想要得到多工切換以提高效能,async會是最好的選擇,再截取一段MSDN的解說來佐證"
非同步對伺服器的幫助
假設我們有兩台伺服器,兩台可服務的執行緒為五條,第一台使用同步的方式,假設在同一時間裡面收到二十條請求,每個要求會執行一個I/O作業,那麼就必須要等到其中一個作業完成了,才會把第六條之後的一一排入佇列處理,但隨著佇列太長,伺服器可能就會變得太慢。
同一例子如果我們使用async的方式來作業, 仍可將第六個要求排入佇列,當 I/O-bound 工作開始時 (而不是完成時),將會釋放其所包含的每個執行緒。 排到第 20 個要求時,傳入要求的佇列會很小 (如果有任何內容),而且伺服器不會變慢。 事實上,您可以預期伺服器使用async和await時,會比針對所收到的每個要求設定專用執行緒,能夠處理更大量的要求。
thread和async的例子
為了証明以上的論點所言不虛,所以一定得動手來做個實驗,最好証實的就是使用win form的方式,請看下面例子,我實做了一個用thread去sleep和task.delay的例子,程式碼如下
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
Thread.Sleep(3000);
}
private async void button2_Click(object sender, EventArgs e)
{
await Task.Delay(3000);
}
}
緊接著我們來看看範例,當我們使用thread.sleep的時候,畫面整個會卡住不能動彈,而async方法則是可以繼續操作
但是其實我們可以使用task.run讓第一個thread.sleep也可以正常操作,把button1的程式碼改成如下
private void button1_Click(object sender, EventArgs e)
{
Task.Run(() => Thread.Sleep(3000));
}
雖然在ui的感覺上是一樣的,不過實際上task.run會從執行緒集區開起另一條thread,而async方法則不會,我們來改寫一下,並實際觀察thread的狀況,以下的例子,就是各自在操作前印出原本的thread和操作後的thread比較
private void button1_Click(object sender, EventArgs e)
{
Debug.WriteLine("orignal:" + Thread.CurrentThread.ManagedThreadId);
Task.Run(() =>
{
Thread.Sleep(3000);
Debug.WriteLine("new task:" + Thread.CurrentThread.ManagedThreadId);
});
}
private async void button2_Click(object sender, EventArgs e)
{
Debug.WriteLine("orignal:" + Thread.CurrentThread.ManagedThreadId);
await Task.Delay(3000);
Debug.WriteLine("async method:" + Thread.CurrentThread.ManagedThreadId);
}
接著我們來觀察一下操作和印出來的狀況
從上圖可以証明,task.delay是不會開起另一條新的thread。
正確自訂一個非同步的方法
雖然上面我是直接寫在事件裡面,但如果我們使用的方法,想要使用非同步的話,理論上以之前的例子來說,會想說使用task.delay的方式就可以了,我們來實驗看看會有什麼狀況
private async void button2_Click(object sender, EventArgs e)
{
Task task1= ExecuteAsyncMethod();
Debug.WriteLine("test");
await task1;
}
private Task ExecuteAsyncMethod()
{
Debug.WriteLine("orignal:" + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(3000);
Debug.WriteLine("async:" + Thread.CurrentThread.ManagedThreadId);
return Task.Delay(10);
}
以上面程式碼的例子來說,我期望會先印出如下訊息
orignal
test
async
但實驗執行下去的狀況,卻是畫面卡住了,而且程式碼也是照著同步的執行順序一步一步走下來的
那如何真正自訂非同步方法呢?請參考如下程式碼實做方式
private async void button2_Click(object sender, EventArgs e)
{
Task task1= ExecuteAsyncMethod();
Debug.WriteLine("test");
await task1;
}
private Task ExecuteAsyncMethod()
{
var tcs = new TaskCompletionSource<bool>();
var fireAndForgetTask = Task.Delay(1)
.ContinueWith(task =>
{
Debug.WriteLine("orignal:" + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(3000);
Debug.WriteLine("async:" + Thread.CurrentThread.ManagedThreadId);
tcs.SetResult(true);
});
return tcs.Task;
}
接著再來看一下最後的執行狀況,我們可以確認畫面一樣正常動作,而且thread也是同一條,並沒有另開thread的狀況哦。
結論
其實我們不一定得要自訂非同步方法,在微軟或一些第三方的library(比如dapper),都會提供一些非同步的方法,比較適合使用非同步的方式,大部份都已經有現成的可以使用了,但我們還是可以了解一下如何自訂,以自行擴充非同步方法,但請必須了解大部份的狀況之下,我們都是運用在計算型的,那麼還是使用task.run的方式來從執行緒集區拉一條thread來執行,當想要自訂非同步方法的時候,必須得先了解一下底層原理,什麼工作是使用i/o在工作的,如果對原理不了解的話,千萬不要貿然的自己做一個非同步方法比較妥當,如果想多了解一些執行緒和非同步相關的議題,筆者也整理了過去寫過相關的系列(https://dotblogs.com.tw/kinanson/series/1?qq=%E5%9F%B7%E8%A1%8C%E7%B7%92%2526%E9%9D%9E%E5%90%8C%E6%AD%A5)