[C#]Multiple thread和非同步的差異,並正確自訂非同步的方式

[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上的一段內容"非同步方法主要做為非封鎖作業使用。 當等候的工作正在執行時,非同步方法的 await 運算式不會封鎖目前的執行緒。 運算式會改為註冊方法的其餘部分做為接續,並將控制權交還給非同步方法的呼叫端。async 和 await 關鍵字不會導致建立其他執行緒。 由於非同步方法不會在本身的執行緒上執行,因此非同步方法不需要多執行緒。 方法會在目前的同步處理內容執行,而且只有在方法為作用中時才會在執行緒上花費時間。 "

硬體上的理論

I/O需要的CPU資源非常的少,大部份工作是交由DMA(Direct Memory Access)完成的,而CPU是不會直接跟硬碟溝通的,他們之間會透過DMA做溝通,當非同步完成之後,DMA會通知CPU任務完成,拿回非同步方法的結果,把一個任務完成,所以簡單來說如果我們想要讓硬體發揮最好的效能,又想要得到多工切換以提高效能,async會是最好的選擇,再截取一段MSDN的解說來佐證" 非同步程式設計的非同步方法幾乎是所有案例的現有方法當中較好的方法。 特別是,這個方法比受限於 IO 之作業的 BackgroundWorker還要好,因為程式碼較簡單,而且不需要防範競爭情況。 "

非同步對伺服器的幫助

假設我們有兩台伺服器,兩台可服務的執行緒為五條,第一台使用同步的方式,假設在同一時間裡面收到二十條請求,每個要求會執行一個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)