我第一次接觸“執行緒”的概念時,覺得它深奧難懂,看了好多本書,花了很長時間才領悟到它的真諦。現在我就以一個初學者的心態,把我所理解的“多執行緒”描述給大家。這一次是系列文章,比較完整的展示與執行緒相關的基本概念。希望對初學者有所幫助。
如果你是高手,請你別繼續看,會浪費你寶貴的時間。
一、基本概念
什麼是程式?
當一個程式開始執行時,它就是一個程式,程式包括執行中的程式和程式所使用到的記憶體和系統資源。 而一個程式又是由多個執行緒所組成的。
什麼是執行緒?
執行緒是程式中的一個執行流,每個執行緒都有自己的專有暫存器(棧指標、程式計數器等),但程式碼區是共享的,即不同的執行緒可以執行同樣的函式。
什麼是多執行緒?
多執行緒是指程式中包含多個執行流,即在一個程式中可以同時執行多個不同的執行緒來執行不同的任務,也就是說允許單個程式建立多個並行執行的執行緒來完成各自的任務。
前臺執行緒後臺執行緒?
應用程式的主執行緒和通過構造一個Thread物件來顯式建立的任何執行緒都預設是前臺執行緒。相反執行緒池執行緒預設為後臺執行緒。另外由進入托管執行環境的本機程式碼建立的任何執行緒都被標記為後臺執行緒。
線上程的生命週期中,任何時候都可以從前臺變為後臺,或者從後臺變為前臺。
前臺執行緒能阻止應用程式的終結。一直到所有的前臺執行緒終止後,CLR才能關閉應用程式(即解除安裝承載的應用程式域)。
後臺執行緒(有時也叫守護執行緒)被CLR認為是程式執行中可做出犧牲的途徑,即在任何時候(即使這個執行緒此時正在執行某項工作)都可能被忽略。因此,如果所有的前臺執行緒終止,當應用程式域解除安裝時,所以的後臺執行緒也會被自動終止。
執行緒是輕量級程式。一個使用執行緒的常見例項是現代作業系統中並行程式設計的實現。使用執行緒節省了 CPU 週期的浪費,同時提高了應用程式的效率。
二、執行緒的生命週期
執行緒生命週期開始於 System.Threading.Thread 類的物件被建立時,結束於執行緒被終止或完成執行時。
執行緒生命週期中的各種狀態:
未啟動狀態:當執行緒例項被建立但 Start 方法未被呼叫時的狀況(將該執行緒標記為可以執行的狀態,但具體執行時間由cpu決定。)。
就緒狀態:當執行緒準備好執行並等待 CPU 週期時的狀況。
不可執行狀態:下面的幾種情況下執行緒是不可執行的:(已經呼叫 Sleep 方法,已經呼叫 Wait 方法,通過 I/O 操作阻塞)
死亡狀態:當執行緒已完成執行或已中止時的狀況。
三、執行緒
1、主執行緒
程式中第一個被執行的執行緒稱為主執行緒
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
using System; using System.Threading; namespace Threading { class Program { static void Main(string[] args) { Thread th = Thread.CurrentThread; th.Name = "MainThread"; Console.WriteLine("This is {0}", th.Name); Console.ReadKey(); } } } |
輸出:This is MainThread
2、執行緒的建立
1 2 3 4 5 6 7 8 9 10 11 12 |
var th = new Thread(new ThreadStart(() => { // })); th.Start(); var ts = new ThreadStart(() => { // }); var th1 = new Thread(ts); th1.Start(); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
using System; using System.Threading; namespace Threading { class Program { public static void Thread1() { Console.WriteLine("Thread1 starts"); } public static void Thread2(object data) { Console.WriteLine("Thread2 starts,para:{0}", data.ToString()); } static void Main(string[] args) { var t1 = new Thread(Thread1); t1.Start(); var t2 = new Thread(Thread2); t2.Start("thread2"); Console.ReadKey(); } } } |
輸入:
Thread1 starts
Thread2 starts,para:thread2
3、執行緒的管理
sleep()掛起和Abort() 銷燬執行緒
通過丟擲 threadabortexception 在執行時中止執行緒。這個異常不能被捕獲,如果有 finally 塊,控制會被送至 finally 塊
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
using System; using System.Threading; namespace Threading { class Program { public static void Thread1() { Console.WriteLine("Thread1 starts"); Console.WriteLine("Thread1 Paused for 5 seconds"); Thread.Sleep(5000); Console.WriteLine("Thread1 resumes"); } static void Main(string[] args) { var t1 = new Thread(Thread1); t1.Start(); Console.ReadKey(); } } } |
執行緒掛起程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
using System; using System.Threading; namespace Threading { class Program { public static void Thread1() { try { Console.WriteLine("Thread1 starts"); for (int i = 0; i 10; i++) { Thread.Sleep(500); Console.WriteLine(i); } Console.WriteLine("Thread1 Completed"); } catch (ThreadAbortException ex) { Console.WriteLine("Thread1 Abort Exception"); } finally { Console.WriteLine("Couldn't catch the Thread1 Exception"); } } static void Main(string[] args) { //開啟子執行緒 var t1 = new Thread(Thread1); t1.Start(); //主執行緒掛起2s Thread.Sleep(2000); //終止t1子執行緒 t1.Abort(); Console.ReadKey(); } } } |
執行緒銷燬程式碼
銷燬程式碼執行結果:
四、執行緒池
在多執行緒程式中,執行緒把大部分的時間花費在等待狀態,等待某個事件發生,然後才能給予響應我們一般用ThreadPool(執行緒池)來解決;執行緒平時都處於休眠狀態,只是週期性地被喚醒我們使用使用Timer(定時器)來解決。
由於執行緒的建立和銷燬需要耗費一定的開銷,過多的使用執行緒會造成記憶體資源的浪費,出於對效能的考慮,於是引入了執行緒池的概念。執行緒池維護一個請求佇列,執行緒池的程式碼從佇列提取任務,然後委派給執行緒池的一個執行緒執行,執行緒執行完不會被立即銷燬,這樣既可以在後臺執行任務,又可以減少執行緒建立和銷燬所帶來的開銷。執行緒池執行緒預設為後臺執行緒。
執行緒池自動管理執行緒執行緒的建立和銷燬。
程式碼展示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
using System; using System.Threading; namespace Threading { class Program { public static void Thread1(object data) { Console.WriteLine("Thread1 => {0}",data.ToString()); } static void Main(string[] args) { //控制執行緒數大小 //第一個引數是:執行緒池中輔助執行緒的最大數目 //第二個引數是:執行緒池中非同步 I/O 執行緒的最大數目 ThreadPool.SetMaxThreads(3, 3); for (int i = 0; i 10; i++) { //ThreadPool是靜態類無需例項化, //ThreadPool.QueueUserWorkItem(new WaitCallback(Thread1), i); ThreadPool.QueueUserWorkItem(Thread1, i); } Console.WriteLine("Thread1 sleep"); Thread.Sleep(100000); Console.WriteLine("Thread1 end"); Console.ReadKey(); } } } |
執行結果:
但是為什麼最開始輸出Thread1 sleep?有時候也會在中間隨機輸出呢?
其實,執行緒池的啟動和終止不是我們程式所能控制的,執行緒池中的執行緒執行完之後是沒有返回值的,我們可以用ManualResetEvent通知一個或多個正在等待的執行緒已發生事件
修改後的程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
using System; using System.Threading; namespace Threading { class Program { //新建ManualResetEvent物件並且初始化為無訊號狀態 private static ManualResetEvent mre = new ManualResetEvent(false); public static void Thread1(object data) { Console.WriteLine("Thread1 => {0}",data.ToString()); if (Convert.ToInt32(data) == 9) { mre.Set(); } } static void Main(string[] args) { //控制執行緒數大小 //第一個引數是:執行緒池中輔助執行緒的最大數目 //第二個引數是:執行緒池中非同步 I/O 執行緒的最大數目 ThreadPool.SetMaxThreads(3, 3); for (int i = 0; i 10; i++) { //ThreadPool是靜態類無需例項化, //ThreadPool.QueueUserWorkItem(new WaitCallback(Thread1), i); ThreadPool.QueueUserWorkItem(Thread1, i); } //阻止當前執行緒,直到當前 WaitHandle 收到訊號為止。 mre.WaitOne(Timeout.Infinite, true); Console.WriteLine("Thread1 sleep"); Thread.Sleep(100000); Console.WriteLine("Thread1 end"); Console.ReadKey(); } } } |
輸入結果:
ok,搞定。
參考資料:
ThreadPool:https://msdn.microsoft.com/zh-cn/library/system.threading.threadpool.aspx#Y0
ManualResetEvent:https://msdn.microsoft.com/zh-cn/library/system.threading.manualresetevent.aspx
五、總結
多執行緒的好處:
可以提高CPU的利用率。在多執行緒程式中,一個執行緒必須等待的時候,CPU可以執行其它的執行緒而不是等待,這樣就大大提高了程式的效率。
多執行緒的不利方面:
執行緒也是程式,所以執行緒需要佔用記憶體,執行緒越多佔用記憶體也越多;
多執行緒需要協調和管理,所以需要CPU時間跟蹤執行緒;
執行緒之間對共享資源的訪問會相互影響,必須解決競用共享資源的問題;
執行緒太多會導致控制太複雜,最終可能造成很多Bug;