多執行緒系列(1),多執行緒基礎

王精靈發表於2020-08-20

執行緒相關概念

在學習多執行緒之前,先來了解下幾個與多執行緒相關的概念。

程式:程式是計算機的概念,程式在伺服器執行時佔據全部計算資源的總和,一個應用程式執行起來就是一個程式,開啟windows的工作管理員,如下圖

執行緒:執行緒也是計算機的概念,執行緒是程式的最小單位,也是程式在響應作業系統時的最小單位,一個程式至少由一個執行緒(主執行緒)構成。執行緒和程式一樣也會佔據一定的CPU、記憶體、網路、硬碟IO等。一個執行緒隸屬於某個程式,程式銷燬,執行緒也隨之銷燬。

控制程式碼:是一個long型別的數字,是作業系統用來標識應用程式的,有點主鍵或者身份證號碼的意思。

多執行緒:一個程式或者說一個應用程式有多個執行緒在執行參與計算。

C#裡面的多執行緒

Thread類是C#語言對執行緒物件的封裝。在.netframework1.0開始出現。在後面的多執行緒系列文章中會講到在不同的.netframework版本中多執行緒的API使用,在本篇文章中,先來初步認識多執行緒。

為什麼可以使用多執行緒

1:CPU的多核技術和模擬核技術:

如計算機的引數概念4核8執行緒,所謂的4核8執行緒,4核指的是物理核心。通過超執行緒技術,用一個物理核模擬兩個虛擬核,每個核兩個執行緒,總數為8執行緒。四核八執行緒採用的超執行緒技術,是指每個CPU核心沒有滿負荷運載時,其剩餘用量可以模擬成虛擬的核心。單個物理核同一時間點只能處理一個執行緒,通過超執行緒技術可以實現單個物理核實現執行緒級別的平行計算。

2:CPU分片:實際上CPU在同一時刻只能處理一個任務,但是因為CPU的計算能力強大,在1秒內可以響應不同的任務,把1秒的處理能力分成10份,1到100毫秒處理任務A,101到200毫秒處理任務B,201到300毫秒處理任務C…,從巨集觀角度來看,感覺多個任務在併發執行,這個就是CPU的分片。

初識同步和非同步

同步方法:發起呼叫,完成後才繼續下一行;非常符合開發思維,有序由上至下執行;

非同步方法:發起呼叫,不用等待完成,直接進入下一行,啟用一個新的執行緒來完成計算。

同步方法就像真誠的請人吃飯,客人說他有事,需要忙一會兒,請吃飯的人等待客人忙完了再一起吃飯。非同步方法就像客氣的請人吃飯,客人說他有事,請吃飯的人說那你先去忙吧,然後自己就去吃飯了。

同步和非同步的比較

同步方法卡介面,主執行緒(UI)執行緒忙於計算,無暇他顧,非同步方法不卡介面:主執行緒閒置,計算任務交給子執行緒完成,改善使用者體驗。如在winform中點選按鈕採用同步的方式呼叫一個複雜的任務計算會導致介面短暫卡死,直到任務計算結束才可以操作介面。

在web應用中發個簡訊通知,記錄一個日誌,都可以採用非同步的方式去執行,客戶端不用等到簡訊傳送成功或者日誌記錄成功才能接受到服務端的響應。

為了能夠清楚的說明情況,這裡採用測試程式對比的方式,測試程式介面如下:

 

計算任務:

        private void DoSomeThing(string btnName) {
            Console.WriteLine($"{btnName} 開始,當前執行緒id:{Thread.CurrentThread.ManagedThreadId}");
            long lResult = 0;
            for (long i = 0; i < 1_000_000_000; i++)
            {
                lResult += i;
            }
            Console.WriteLine($"{btnName} 結束,當前執行緒id:{Thread.CurrentThread.ManagedThreadId}");
        }

同步方式呼叫:

        private void BtnSync_Click(object sender, EventArgs e)
        {
            Console.WriteLine($"btnSync_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")}" +
                $" {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();
            for (int i = 0; i < 5; i++)
            {
                string name = string.Format($"btnSync_Click_{i}");
                this.DoSomeThing(name);
            }
            stopwatch.Stop();
            Console.WriteLine($"同步方法耗時:{stopwatch.ElapsedMilliseconds}");
            Console.WriteLine($"DoSomeThing End {Thread.CurrentThread.ManagedThreadId.ToString("00")} " +
                $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
        }

同步方式呼叫執行結果:

 

同步方式呼叫時CPU的使用情況:

非同步方式呼叫:

        private void BtnAsync_Click(object sender, EventArgs e)
        {
            Console.WriteLine($"btnAsync_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")}" +
                $" {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
            Stopwatch stopwatch = new Stopwatch();
            List<Task> tasks = new List<Task>();
            stopwatch.Start();
            for (int i = 0; i < 5; i++)
            {
                tasks.Add(Task.Run(()=> {
                    string name = string.Format($"btnAsync_Click{i}");
                    this.DoSomeThing(name);
                })); 
            }
            Task.Run(()=> {
                Task.WaitAll(tasks.ToArray());
                stopwatch.Stop();
                Console.WriteLine($"非同步方法耗時:{stopwatch.ElapsedMilliseconds}");
                Console.WriteLine($"DoSomeThing   End {Thread.CurrentThread.ManagedThreadId.ToString("00")}" +
                    $" {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
            });
        }

非同步方式呼叫執行結果

 

非同步方式呼叫時CPU的使用情況:

同步方法慢,上圖耗時(16440毫秒),因為只有一個執行緒計算。非同步方法快,上圖耗時(9435毫秒),因為有多個執行緒參與計算。觀察同步和非同步呼叫時的使用情況折線圖分析得知:多執行緒其實就是資源換取效能。在一個應用程式中是不是開啟的執行緒越多越好?不是的,因為開啟更多的資源,需要消耗更多的計算機資源,資源是有限的、資源排程也需要消耗資源,就像專案經理管理開發人員保證專案進度,專案經理排程(管理)開發人員也是需要工資的。

一個訂單表統計很耗時間,能用多執行緒優化效能麼?不能!這就是一個操作,沒辦法平行計算,就像一個老師不能同時在兩個班級講課。如果一個操作在查詢資料庫的同時,需要呼叫介面、讀寫硬碟檔案、做資料計算,這個可以用多執行緒優化效能,因為多個任務可以平行計算。

同步方法有序進行,非同步多執行緒無序

啟動無序:執行緒資源是屬於非託管資源,是程式向作業系統申請的,由作業系統的排程策略決定,所以啟動順序是隨機的,cpu使用同一個執行緒計算同一個任務,執行時間也是不確定的,so,結束也是無序的。在使用多執行緒的時候一定要小心,尤其是多執行緒間有順序要求的時候通過延遲一點時間(Thread.Sleep())來控制執行順序,這是不靠譜的。

相關文章