1. 程式人生 > 其它 >C# 多執行緒入門系列(二)

C# 多執行緒入門系列(二)

執行緒(英語:thread)是作業系統能夠進行運算排程的最小單位。它被包含在程序之中,是程序中的實際運作單位。一條執行緒指的是程序中一個單一順序的控制流,一個程序中可以併發多個執行緒,每條執行緒並行執行不同的任務。程序是資源分配的基本單位。所有與該程序有關的資源,都被記錄在程序控制塊PCB中。以表示該程序擁有這些資源或正在使用它們。本文以一些簡單的小例子,簡述多執行緒的發展歷程【Thread,ThreadPool,Task,Parallel】,僅供學習分享使用,如有不足之處,還請指正。

Thread

Thread做為早期【.Net Framework1.0】的.Net提供的多執行緒方案,提供了很多的封裝方法,來操作執行緒。具體如下所示:

  1. Start方法,用於啟動一個執行緒。執行緒的狀態變更為Running。
  2. Suspend方法,掛起一個執行緒,或者如果執行緒狀態為已掛起,則不起作用。
  3. Resume方法,如果執行緒為已掛起,這繼續執行。
  4. Join方法,等待執行緒,直到執行緒結束,也可以設定等待時間。
  5. Abort方法,強制終止執行緒,跑出ThreadAbortException異常。
  6. 其他執行緒屬性:IsBackground是否後臺執行緒,IsThreadPoolThread是否執行緒池執行緒,IsAlive執行緒是否執行,Priority執行緒優先順序,ThreadState當前執行緒狀態,ManagedThreadId執行緒唯一標識等

通過Thread可以單獨的開啟一個執行緒,通過建構函式來建立執行緒物件,可以是無引數也可以是帶引數。其中引數ThreadStart是一個無引數委託,ParameterizedThreadStart為一個帶引數委託。

無引數,示例如下所示:

 1 private void btnThread_Click(object sender, EventArgs e)
 2 {
 3     ThreadStart threadStart = new ThreadStart(DoSomethingLong);
 4     Thread thread = new Thread(threadStart);
5 thread.Start(); 6 } 7 8 private void DoSomethingLong() { 9 string name = "Thread"; 10 Console.WriteLine("************DoSomethingLong 開始 name= {0} 執行緒ID= {1} 時間 = {2}************", name, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff")); 11 //CPU計算累加和 12 long rest = 0; 13 for (int i = 0; i < 1000000000; i++) 14 { 15 rest += i; 16 } 17 Console.WriteLine("************DoSomethingLong 結束 name= {0} 執行緒ID= {1} 時間 = {2} 結果={3}************", name, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff"), rest); 18 }

示例結果如下所示:

帶引數示例,如下所示:

 1 private void btnThread2_Click(object sender, EventArgs e)
 2 {
 3     ParameterizedThreadStart threadStart = new ParameterizedThreadStart(DoSomethingLongWithParam);
 4     Thread thread = new Thread(threadStart);
 5     string name = "Param";
 6     thread.Start(name);
 7 }
 8 
 9 private void DoSomethingLongWithParam(object name) {
10     Console.WriteLine("************DoSomethingLongWithParam 開始 name= {0} 執行緒ID= {1} 時間 = {2}************", name, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff"));
11     //CPU計算累加和
12     long rest = 0;
13     for (int i = 0; i < 1000000000; i++)
14     {
15         rest += i;
16     }
17     Console.WriteLine("************DoSomethingLongWithParam 結束 name= {0} 執行緒ID= {1} 時間 = {2} 結果={3}************", name, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff"), rest);
18 }

帶引數示例結果,如下所示:

通過對以上示例進行分析,得出結論如下所示:

  1. 執行緒是由作業系統進行建立的,Thread提供的方法只是對底層方法的封裝。比如執行對應方法後,作業系統並不會立即執行相應的操作,而要CPU時間片輪轉後才會執行響應。
  2. Thread建立執行緒過於鬆散,缺乏管理,例如,如果同時建立10000個執行緒,程式也不會報錯,但是系統可能無法承載如此多的執行緒而導致崩潰。
  3. Thread的頻繁建立和銷燬,也會消耗系統資源。

ThreadPool

為了應對Thread建立缺乏管理的問題,在後續版本【.Net Framework2.0】中推出了執行緒池的概念。那什麼是執行緒池呢?

池化資源管理設計思想:執行緒是一種資源,之前每次需要執行緒,都是去建立執行緒,使用完成後,再釋放掉。池化,就是做一個容器,容器提前申請指定數量的執行緒,需要用到執行緒的時候,直接到執行緒池中取,用完之後再放回容器【通過控制狀態標識執行緒是否正在被使用】。避免頻繁的建立和銷燬,容器還會根據限制的數量取申請和釋放。

關於通過執行緒池建立多執行緒,具體示例如下:

1 private void btnThread3_Click(object sender, EventArgs e)
2 {
3     WaitCallback waitCallback = new WaitCallback(DoSomethingLongWithParam);
4     string name = "ThreadPool";
5     ThreadPool.QueueUserWorkItem(waitCallback,name);
6 }

執行緒池示例執行結果如下所示:

通過對執行緒池執行結果進行分析,得出結論如下:

  1. 兩次執行結果,均為同一個執行緒ID,說明執行緒用完並未銷燬,而是放回執行緒池子,待下次使用時重新取出,繼續使用。
  2. 通過執行緒池可以有效的控制執行緒併發的數量,避免資源的浪費。
  3. 通過分析原始碼發現,ThreadPool執行緒池提供的介面較少,線上程等待和互動方面不太友好。

Task

隨著.Net版本的演化,後續版本【.Net Framework3.0】推出了Task做為多執行緒解決方案。預設情況下,可以通過建構函式建立Task,示例如下:

1 private void btnTask_Click(object sender, EventArgs e)
2 {
3     Action action = new Action(DoSomethingLong);
4     Task task = new Task(action);
5     task.Start();
6 }

預設Task示例,執行結果如下:

通過對以上Task示例和原始碼進行分析,得出結論如下:

  1. Task產生的執行緒,全部都是執行緒池執行緒。
  2. Task提供的豐富的API,便於開發實踐。

Parallel

Parallel提供對並行執行緒的支援,可以通過Parallel同時發起多個執行緒,在某些方面具有應用優勢,預設示例如下所示:

1 private void btnParallel_Click(object sender, EventArgs e)
2 {
3     Action action = new Action(DoSomethingLong);
4     Parallel.Invoke(action,action,action);
5 }

Parallel的Invoke方法執行,結果如下:

通過對Parallel的Invoke示例方法進行分析,得出結論如下:

  1. Parallel的Invoke方法,可以同時開啟多個執行緒,同時主執行緒【執行緒ID=1】也會參與計算,即頁面也會卡住。
  2. Parallel可以通過ParallelOptions.MaxDegreeOfParallelism指定併發數量。

Task專講

以下面的一個場景為例進行說明:

假如開發一個系統,流程如下:
1. 前期的需求調研,需求分析,系統設計,詳細設計(順序執行,是開發編碼的前提)
2.按模組開發【中間階段,可多人同時工作】
3.測試【順序執行,是開發編碼的後續工作】
分析:以上三個階段,每一個階段又可以細分數個小階段,其中有些階段是順序執行的,有些階段又可以並行執行。

以程式碼的形式進行描述,如下所示:

private void btnTask2_Click(object sender, EventArgs e)
{
    //開發前的工作
    Console.WriteLine("組建團隊");
    Console.WriteLine("需求分析");
    Console.WriteLine("系統設計");
    Console.WriteLine("詳細設計");
    //開始開發
    Task.Run(() => { Coding("張三", "介面"); });
    Task.Run(() => { Coding("李四", "前端頁面"); });
    Task.Run(() => { Coding("王五", "手機App"); });
    Task.Run(() => { Coding("劉大", "後端業務"); });
    //開發後的工作
    Console.WriteLine("alpha測試");
    Console.WriteLine("beta測試");
    Console.WriteLine("uat測試");
    Console.WriteLine("系統上線");
}

private void Coding(string developer,string model) {
    Console.WriteLine("【Begin】在{0},{1}開始開發{2},執行緒id為{3}", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"), developer,model, Thread.CurrentThread.ManagedThreadId);
    Thread.Sleep(1000);
    Console.WriteLine("【 End 】在{0},{1}完成開發{2},執行緒id為{3}", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"), developer, model, Thread.CurrentThread.ManagedThreadId);
}

示例執行結果,如下所示:

通過分析以上示例,發現程式並未按照預期的執行,很明顯的一點:測試跑到了開發前面。

為了解決上述順序錯亂的問題,Task提供了WaitAll方法,如下所示:

 1 private void btnTask2_Click(object sender, EventArgs e)
 2 {
 3     //開發前的工作
 4     Console.WriteLine("組建團隊");
 5     Console.WriteLine("需求分析");
 6     Console.WriteLine("系統設計");
 7     Console.WriteLine("詳細設計");
 8     //開始開發
 9     List<Task> tasks = new List<Task>();
10     tasks.Add(Task.Run(() => { Coding("張三", "介面"); }));
11     tasks.Add(Task.Run(() => { Coding("李四", "前端頁面"); }));
12     tasks.Add(Task.Run(() => { Coding("王五", "手機App"); }));
13     tasks.Add(Task.Run(() => { Coding("劉大", "後端業務"); }));
14     Task.WaitAll(tasks.ToArray());
15     //開發後的工作
16     Console.WriteLine("alpha測試");
17     Console.WriteLine("beta測試");
18     Console.WriteLine("uat測試");
19     Console.WriteLine("系統上線");
20 }

執行示例,結果如下所示:

通過執行以上示例,發現:順序確實符合預期,可以滿足要求,但是程式會卡住,這點不太友好

如何才能優雅的控制先後順序呢?Task還提供了TaskFactory,如下所示:

 1 private void btnTask2_Click(object sender, EventArgs e)
 2 {
 3     //開發前的工作
 4     Console.WriteLine("組建團隊");
 5     Console.WriteLine("需求分析");
 6     Console.WriteLine("系統設計");
 7     Console.WriteLine("詳細設計");
 8     //開始開發
 9     List<Task> tasks = new List<Task>();
10     tasks.Add(Task.Run(() => { Coding("張三", "介面"); }));
11     tasks.Add(Task.Run(() => { Coding("李四", "前端頁面"); }));
12     tasks.Add(Task.Run(() => { Coding("王五", "手機App"); }));
13     tasks.Add(Task.Run(() => { Coding("劉大", "後端業務"); }));
14     TaskFactory taskFactory = new TaskFactory();
15     taskFactory.ContinueWhenAll(tasks.ToArray(), new Action<Task[]>((taskArray) => { 
16         //開發後的工作
17         Console.WriteLine("alpha測試");
18         Console.WriteLine("beta測試");
19         Console.WriteLine("uat測試");
20         Console.WriteLine("系統上線");
21     }));
22     
23 }

TaskFactory示例測試,如下所示:

通過對示例進行分析,得出如下結論:

  1. 業務邏輯上已要求,頁面也不會卡頓,優雅的實現了多執行緒的操作。
  2. Task產生的執行緒,為執行緒池執行緒。
  3. TaskFactory不僅提供了ContinueWhenAll等待所有執行緒,還提供了ContinueWhenAny等待任意執行緒。

備註

以上就是多執行緒的基礎知識,旨在拋磚引玉,一起學習,共同進步。

古從軍行【作者】李頎【朝代】唐

白日登山望烽火,黃昏飲馬傍交河。行人刁斗風沙暗,公主琵琶幽怨多。

野雲萬里無城郭,雨雪紛紛連大漠。胡雁哀鳴夜夜飛,胡兒眼淚雙雙落。

聞道玉門猶被遮,應將性命逐輕車。年年戰骨埋荒外,空見蒲桃入漢家。

出處:https://www.cnblogs.com/hsiang/p/15690604.html

您的資助是我最大的動力!
金額隨意,歡迎來賞!
款後有任何問題請給我留言。

如果,您認為閱讀這篇部落格讓您有些收穫,不妨點選一下右下角的推薦按鈕。
如果,您希望更容易地發現我的新部落格,不妨點選一下綠色通道的關注我。(●'◡'●)

如果對你有所幫助,贊助一杯咖啡!打 付款後有任何問題請給我留言!!!

因為,我的寫作熱情也離不開您的肯定與支援,感謝您的閱讀,我是【Jack_孟】!