C# 多執行緒入門系列(二)
執行緒(英語:thread)是作業系統能夠進行運算排程的最小單位。它被包含在程序之中,是程序中的實際運作單位。一條執行緒指的是程序中一個單一順序的控制流,一個程序中可以併發多個執行緒,每條執行緒並行執行不同的任務。程序是資源分配的基本單位。所有與該程序有關的資源,都被記錄在程序控制塊PCB中。以表示該程序擁有這些資源或正在使用它們。本文以一些簡單的小例子,簡述多執行緒的發展歷程【Thread,ThreadPool,Task,Parallel】,僅供學習分享使用,如有不足之處,還請指正。
Thread
Thread做為早期【.Net Framework1.0】的.Net提供的多執行緒方案,提供了很多的封裝方法,來操作執行緒。具體如下所示:
- Start方法,用於啟動一個執行緒。執行緒的狀態變更為Running。
- Suspend方法,掛起一個執行緒,或者如果執行緒狀態為已掛起,則不起作用。
- Resume方法,如果執行緒為已掛起,這繼續執行。
- Join方法,等待執行緒,直到執行緒結束,也可以設定等待時間。
- Abort方法,強制終止執行緒,跑出ThreadAbortException異常。
- 其他執行緒屬性: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 }
帶引數示例結果,如下所示:
通過對以上示例進行分析,得出結論如下所示:
- 執行緒是由作業系統進行建立的,Thread提供的方法只是對底層方法的封裝。比如執行對應方法後,作業系統並不會立即執行相應的操作,而要CPU時間片輪轉後才會執行響應。
- Thread建立執行緒過於鬆散,缺乏管理,例如,如果同時建立10000個執行緒,程式也不會報錯,但是系統可能無法承載如此多的執行緒而導致崩潰。
- 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 }
執行緒池示例執行結果如下所示:
通過對執行緒池執行結果進行分析,得出結論如下:
- 兩次執行結果,均為同一個執行緒ID,說明執行緒用完並未銷燬,而是放回執行緒池子,待下次使用時重新取出,繼續使用。
- 通過執行緒池可以有效的控制執行緒併發的數量,避免資源的浪費。
- 通過分析原始碼發現,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示例和原始碼進行分析,得出結論如下:
- Task產生的執行緒,全部都是執行緒池執行緒。
- 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示例方法進行分析,得出結論如下:
- Parallel的Invoke方法,可以同時開啟多個執行緒,同時主執行緒【執行緒ID=1】也會參與計算,即頁面也會卡住。
- 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示例測試,如下所示:
通過對示例進行分析,得出如下結論:
- 業務邏輯上已要求,頁面也不會卡頓,優雅的實現了多執行緒的操作。
- Task產生的執行緒,為執行緒池執行緒。
- TaskFactory不僅提供了ContinueWhenAll等待所有執行緒,還提供了ContinueWhenAny等待任意執行緒。
備註
以上就是多執行緒的基礎知識,旨在拋磚引玉,一起學習,共同進步。
古從軍行【作者】李頎
白日登山望烽火,黃昏飲馬傍交河。行人刁斗風沙暗,公主琵琶幽怨多。
野雲萬里無城郭,雨雪紛紛連大漠。胡雁哀鳴夜夜飛,胡兒眼淚雙雙落。
聞道玉門猶被遮,應將性命逐輕車。年年戰骨埋荒外,空見蒲桃入漢家。
出處:https://www.cnblogs.com/hsiang/p/15690604.html
您的資助是我最大的動力!
金額隨意,歡迎來賞!
付款後有任何問題請給我留言。
【關注我】。(●'◡'●)
如果對你有所幫助,贊助一杯咖啡!打 如果,您希望更容易地發現我的新部落格,不妨點選一下綠色通道的 付款後有任何問題請給我留言!!!因為,我的寫作熱情也離不開您的肯定與支援,感謝您的閱讀,我是【Jack_孟】!