在多線程編程中,線程的創建和銷毀是非常消耗系統資源的,因此,C#引入了池的概念,類似的還有數據庫連接池,這樣,維護一個池,池內維護的一些線程,需要的時候從池中取出來,不需要的時候放回去,這樣就避免了重復創建和銷毀線程。
ThreadPool類 MSDN幫助信息: http://msdn.microsoft.com/zh-cn/library/system.threading.threadpool.aspx#Y0
將任務添加進線程池:
ThreadPool.QueueUserWorkItem(new WaitCallback((方法名)); ThreadPool.QueueUserWorkItem(new WaitCallback((方法名),傳入方法的參數);
對線程池的線程數量進行控制
SetMaxThreads(Int32, Int32) //設置可以同時處於活動狀態的線程池的請求數目。所有大於此數目的請求將保持排隊狀態,直到線程池線程變為可用。 SetMinThreads(Int32, Int32) //發出新的請求時,在切換到管理線程創建和銷毀的算法之前設置線程池按需創建的線程的最小數量。
對線程池線程數量控制的驗證
public static void Main() { ThreadPool.SetMinThreads(1, 1); ThreadPool.SetMaxThreads(1, 1); for (int i = 0; i < 10; i++) { ThreadPool.QueueUserWorkItem(new WaitCallback((object obj) => { Console.WriteLine("當前值為:" + obj + "線程ID" + Thread.CurrentThread.ManagedThreadId); }),i); } Console.Read(); }
最大線程數量和最小線程數量全部設置為1,上述代碼的執行結果為:
可以看到只開啟了一個線程。將最大線程改為2
public static void Main() { ThreadPool.SetMinThreads(1, 1); ThreadPool.SetMaxThreads(2, 2); for (int i = 0; i < 10; i++) { ThreadPool.QueueUserWorkItem(new WaitCallback((object obj) => { Console.WriteLine("當前值為:" + obj + "線程ID" + Thread.CurrentThread.ManagedThreadId); }),i); } Console.Read(); }
此時啟動了兩個線程
但是這最多和最少並不是說一定要使用這麼多線程的,比如,我設置最少10個線程,但是實際上可能只試用了3-4個,但是線程池中確實是最少會維護著10個線程,不一定每次全部都啟用的。
public static void Main() { ThreadPool.SetMinThreads(10, 10); ThreadPool.SetMaxThreads(20, 20); Console.WriteLine("測試開始!"); for (int i = 0; i < 10; i++) { ThreadPool.QueueUserWorkItem(new WaitCallback((object obj) => { Console.WriteLine("當前值為:" + obj + "線程ID" + Thread.CurrentThread.ManagedThreadId); }),i); } Console.WriteLine("測試結束!"); Console.Read(); }
上面的執行結果:
為什麼打印測試結束的語句執行的這麼靠前呢?這是什麼原因呢?
這是因為在循環中將任務添加到線程池中後,並沒有等待線程執行完成再繼續執行主線程,也就是線程池中的現成是如何啟動及結束我們是不知道的,ThreadPool沒有提供簡單的方法來獲取工作線程是否已經結束,所以需要通過其他方法實現。此時,需要引入ManualResetEvent類,MSDN:https://msdn.microsoft.com/zh-cn/library/system.threading.manualresetevent.aspx,
ManualResetEvent 允許線程通過發信號互相通信。通常,此通信涉及一個線程在其他線程進行之前必須完成的任務。
當一個線程開始一個活動(此活動必須完成後,其他線程才能開始)時,它調用 Reset 以將 ManualResetEvent 置於非終止狀態。此線程可被視為控制 ManualResetEvent。調用 ManualResetEvent 上的 WaitOne 的線程將阻止,並等待信號。當控制線程完成活動時,它調用 Set 以發出等待線程可以繼續進行的信號。並釋放所有等待線程。
一旦它被終止,ManualResetEvent 將保持終止狀態,直到它被手動重置。即對 WaitOne 的調用將立即返回。
可以通過將布爾值傳遞給構造函數來控制 ManualResetEvent 的初始狀態,如果初始狀態處於終止狀態,為 true;否則為 false。
方法WaitOne(Timeout.Infinite, true); 阻止當前線程,直到當前 WaitHandle 收到信號為止。
方法Set(); 將事件狀態設置為終止狀態,允許一個或多個等待線程繼續。
這段話到底是什麼意思呢?我們通過一段代碼來理解
public static void Main() { ThreadPool.SetMinThreads(3,3); ManualResetEvent mre = new ManualResetEvent(false); for (int i = 0; i < 3; i++) { ThreadPool.QueueUserWorkItem(new WaitCallback((object obj) => { Console.WriteLine("線程 " + obj.ToString() + " 已啟動! 線程ID為:" + Thread.CurrentThread.ManagedThreadId ); mre.WaitOne(); Thread.Sleep(500); Console.WriteLine("線程 " + obj.ToString() + " 已結束!線程ID為:" + Thread.CurrentThread.ManagedThreadId ); }), i); } Console.Read(); }
上述代碼的執行結果是:
從執行結果中可以看到,我們往線程池中添加了三個任務,線程池啟用了三個線程去執行。當任務方法執行到mre.WaitOne();時,線程被ManualResetEvent 阻止,並沒有繼續往下走,也就是此時線程們正在等待一個信號,下面我們就給線程們發出這個信號。
public static void Main() { ThreadPool.SetMinThreads(3,3); ManualResetEvent mre = new ManualResetEvent(false); for (int i = 0; i < 3; i++) { ThreadPool.QueueUserWorkItem(new WaitCallback((object obj) => { Console.WriteLine("線程 " + obj.ToString() + " 已啟動! 線程ID為:" + Thread.CurrentThread.ManagedThreadId ); mre.WaitOne(); Thread.Sleep(500); Console.WriteLine("線程 " + obj.ToString() + " 已結束!線程ID為:" + Thread.CurrentThread.ManagedThreadId); }), i); } if (Console.ReadLine() == "go") { mre.Set(); } Console.Read(); }
三個新線程雖然被阻止,但是主線程是可以繼續執行的,當主線程收到用戶輸入的go命令時,給三個線程發送信號,線程們收到信號後繼續執行,並打印出執行結束的標識。
相信到這裡我們應該能夠理解WaitOne和Set的用法了,下面我們在看看Reset方法,我們在mre.Set()後,在開啟三個新的線程
public static void Main() { ThreadPool.SetMinThreads(3,3); ManualResetEvent mre = new ManualResetEvent(false); for (int i = 0; i < 3; i++) { ThreadPool.QueueUserWorkItem(new WaitCallback((object obj) => { Console.WriteLine("線程 " + obj.ToString() + " 已啟動! 線程ID為:" + Thread.CurrentThread.ManagedThreadId ); mre.WaitOne(); Thread.Sleep(600); Console.WriteLine("線程 " + obj.ToString() + " 已結束!線程ID為:" + Thread.CurrentThread.ManagedThreadId); }), i); } if (Console.ReadLine() == "go") { mre.Set(); } Thread.Sleep(1000); Console.WriteLine("*******************************再開啟三個線程***********************************"); for (int i = 3; i < 6; i++) { ThreadPool.QueueUserWorkItem(new WaitCallback((object obj) => { Console.WriteLine("線程 " + obj.ToString() + " 已啟動! 線程ID為:" + Thread.CurrentThread.ManagedThreadId); mre.WaitOne(); Thread.Sleep(600); Console.WriteLine("線程 " + obj.ToString() + " 已結束!線程ID為:" + Thread.CurrentThread.ManagedThreadId); }), i); } Console.Read(); }
運行代碼看看效果
新開的線程和之前開的線程的任務方法是一模一樣的啊,為什麼沒有等待信號而直接繼續運行了呢?
這是因為我們在調用ManualResetEvent的Set方法後,在調用其 Reset 方法前會一直保持終止狀態,所以,新線程任務方法中的WaitOne是無效的,因為此時ManualResetEvent是終止狀態的。下面我們加上Reset方法看看效果
public static void Main() { ThreadPool.SetMinThreads(3,3); ManualResetEvent mre = new ManualResetEvent(false); for (int i = 0; i < 3; i++) { ThreadPool.QueueUserWorkItem(new WaitCallback((object obj) => { Console.WriteLine("線程 " + obj.ToString() + " 已啟動! 線程ID為:" + Thread.CurrentThread.ManagedThreadId ); mre.WaitOne(); Thread.Sleep(600); Console.WriteLine("線程 " + obj.ToString() + " 已結束!線程ID為:" + Thread.CurrentThread.ManagedThreadId); }), i); } if (Console.ReadLine() == "go") { mre.Set(); } Thread.Sleep(1000); Console.WriteLine("*******************************再開啟三個線程***********************************"); mre.Reset(); for (int i = 3; i < 6; i++) { ThreadPool.QueueUserWorkItem(new WaitCallback((object obj) => { Console.WriteLine("線程 " + obj.ToString() + " 已啟動! 線程ID為:" + Thread.CurrentThread.ManagedThreadId); mre.WaitOne(); Thread.Sleep(600); Console.WriteLine("線程 " + obj.ToString() + " 已結束!線程ID為:" + Thread.CurrentThread.ManagedThreadId); }), i); } if (Console.ReadLine() == "go") { mre.Set(); } Console.Read(); }
執行結果:
此時,達到了我們想要的結果。
最後調用 mre.Close();釋放資源即可。
現在明白了ManualResetEvent類的使用,想要解決開始的“測試開始、測試結束”打印順序問題就是張飛吃豆芽了吧?!