在我們開發程序時,若存在耗性能、高並發處理的任務時,我們會想到用多線程來處理。在多線程處理中,有手工創建線程與線程池2種處理方式,手工創建線程存在管理與維護的繁瑣。.Net線程池能夠幫我們完成線程資源的管理工作,使用我們專注業務處理,而不是代碼的細微實現。在你創建了過多的任務,線程池也能用列隊把無法即使處理的請求保存起來,直至有線程釋放出來。
當應用程序開始執行重復的后台任務,且並不需要經常與這些任務交互時,使用.Net線程池管理這些資源將會讓性能更佳。我們可以使用ThreadPool.QueueUserWorkItem方法來讓線程池為你管理資源,將方法資源排入隊列以便執行。 此方法在有線程池線程變得可用時執行。QueueUserWorkItem方法有2中重載,分別為有參數與無參數。如下列出:

線程池會根據正在運行的任務數量與線程池大小,被加入的任務可能會立即執行,或等待直至有空余的線程再處理。線程池由每個處理器中一定數量的就緒線程和一系列I/O讀取線程組成,具體的數字因硬件與.Net版本不同而有差別,在開始向列隊中插入執行的任務時,線程池可能會創建更多的線程,也可能等待有可用線程再去執行,這取決於當前內存與其他資源的可用情況。我們不需要詳細明白線程池內部的具體實現,因為線程池本身就是為了降低我們的工作,並讓框架幫我們分擔。簡而言之,線程池中的線程數量將在可用線程數據和最小化已分配但尚未使用的資源之間自動平衡。
線程池同樣也會管理線程結束后的維護工作,當任務結束后,線程並不會被銷毀,而是返回到可用狀態,以便執行其他任務。所有的QueueUserWorkItem使用的線程池中的線程均為后台線程,這就意味着你並不需要在應用程序推出之前手工清理資源,若是應用程序在這些后台線程還在運行時就退出了,那么系統將會停止這些后台任務,並釋放所有與應用程序相關的資源。我們只要確保在應用程序退出之前停止了所有非后台線程即可。
如下給出了3種線程的測試代碼,分別為單個線程、手工線程、線程池
/// <summary> /// 測試單線程、手工線程、線程池 /// </summary> public static class TestThread { private static uint lowerBound = 0, upperBound = 1000000; private const double tolerance = 1.0e-8;//公差 // 獲得數字的平方根 private static double SquareRoot(double number) { double guess = 1, error = Math.Abs(guess * guess - number); while (error > tolerance) { guess = (number / guess + guess) / 2; error = Math.Abs(guess * guess - number); } return guess; } public static double SingleThread() { Stopwatch start = new Stopwatch(); start.Start(); for (uint i = lowerBound; i < upperBound; i++) { double answer = SquareRoot(i); } start.Stop(); return start.ElapsedMilliseconds; } public static double ManualThreads(int numThreads) { Stopwatch start = new Stopwatch(); using (AutoResetEvent e = new AutoResetEvent(false)) { int workerThreads = numThreads; start.Start(); for (int thread = 0; thread < numThreads; thread++) { Thread t = new Thread(() => { for (uint i = lowerBound; i < upperBound; i++) { //並行計算 if (i % numThreads == thread) { double answer = SquareRoot(i); } } //減少計數器的值 if (Interlocked.Decrement(ref workerThreads) == 0) { e.Set();//設置事件 } }); t.Start(); } //等待信號 e.WaitOne(); start.Stop(); return start.ElapsedMilliseconds; } } public static string content = "線程池輸出的內容"; public static double ThreadPoolThreads(int numThreads) { Stopwatch start = new Stopwatch(); //此處使用AutoResetEvent類,用於通知當前線程(等待的線程,即主線程)發生了什么。 using (AutoResetEvent e = new AutoResetEvent(false))//false標記為非終止狀態,即任務未完成 { int workerThreads = numThreads; start.Start(); for (int thread = 0; thread < numThreads; thread++) { /*WaitCallback,回調委托,代表由系統(程序)自動執行的方法,不需要自己手動去執行。在使用QueueUserWorkItem單個參數方法時,WaitCallback委托中state參數為null;若要使用state參數進行數據處理,需要調用2個參數的QueueUserWorkItem方法。*/ ThreadPool.QueueUserWorkItem(x => { Console.WriteLine(content); for (uint i = lowerBound; i < upperBound; i++) { //並行計算 if (i % numThreads == thread) { double answer = SquareRoot(i); } } //遞減計數器的值,Interlocked類是連鎖-互鎖,為多線程共享的資源在並發時,鎖定共享的資源只能同時有一個線程在執行。此處也可以使用lock(關鍵字)同步鎖進行處 if (Interlocked.Decrement(ref workerThreads) == 0) { //設置事件狀態為終止狀態,即任務完成(告訴等待的線程不用等待,可以繼續執行了)。 e.Set(); } }, content); } //等待信號,阻塞當前線程(直到任務完成-即AutoResetEvent設置為終止狀態,才繼續執行) e.WaitOne(); start.Stop(); return start.ElapsedMilliseconds; } } }
下面是控制台主方法的代碼,及輸出的內容。
static void Main(string[] args) { try { double result = TestThread.SingleThread(); Console.WriteLine("單個線程執行10回百萬個數字的平方根的耗時:{0}ms", result * 10); result = TestThread.ManualThreads(10); Console.WriteLine("手工線程執行10回百萬個數字的平方根的耗時:{0}ms", result); result = TestThread.ThreadPoolThreads(10); Console.WriteLine("線程池執行10回百萬個數字的平方根的耗時:{0}ms", result); } catch (Exception ex) { Console.WriteLine(ex); } Console.Read(); }

以上輸出的耗時結果所使用的CPU型號是Inter(R) Core(TM) i3-3220 CPU @3.30GHz
從上面的耗時結果"單個線程>手工線程>線程池"可以看出使用線程給算法帶來的影響。之所以線程池的實現要優於手工創建線程,主要有2個因素。
-
線程池將重用那些被釋放了的線程,而手工創建線程時,必須為每個任務創建一個全新的線程,線程的創建與銷毀所花費的時間要高於.Net線程池管理所帶來的開銷。
-
線程池將為你管理活動線程的數量,若創建了過多的線程,那么系統將掛起一部分,直到有足夠的資源執行,QueueUserWorkItem則將工作交給線程池中接下來的一個可用線程,並幫你完成一定的線程管理工作。若應用程序的線程池中所有的線程均被占用,那么線程池也會掛起任務,直至出現可用線程。
我們在開發.Net服務端應用程序時,例如WCF、ASP.Net、.Net遠程處理等,都會或多或少的要用到多線程,這些.Net子系統均使用了線程池來管理線程,因此我們也應該采用這種做法。線程池能夠降低額外開銷,進而提高性能。
