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等待任意線程。

備注

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

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

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

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

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM