前言:看了書上兩個使用C#4.0並行編程的demo,又對照以前收藏的網上幾篇講述線程池的雄文,一併整理,寫個樣本總結一下。寫這篇文章的時候,發現關於線程的好幾個基礎的重要的知識點自己都不熟悉,而且可能習慣性認知淺薄,所以痛苦的無以複加,不知道到底要說什麼。不想看文章的可以直接下載最後的樣本,本文代碼主要參考Marc Clifton的“.NET's ThreadPool Class - Behind The Scenes”,對新手也許有協助。
參考:
http://msdn.microsoft.com/zh-cn/library/system.threading.threadpool(VS.80).aspx
http://www.codeproject.com/KB/threads/threadtests.aspx
http://www.codeproject.com/KB/threads/smartthreadpool.aspx
http://blog.zhaojie.me/2009/07/thread-pool-1-the-goal-and-the-clr-thread-pool.html (老趙的淺談線程池上中下三篇)
Jeffrey Richter <<CLR via C#>> 3rd Edition
先大概看一下控制台應用程式的Main方法的主要代碼:
static bool done = false; static decimal count2 = 0; static int threadDone = 0;//標誌啟用線程數? static System.Timers.Timer timer = new System.Timers.Timer(1000); static decimal[] threadPoolCounters = new decimal[10]; static Thread[] threads = new Thread[10]; static System.Timers.Timer[] threadTimers = new System.Timers.Timer[10]; static void Main(string[] args) { timer.Stop(); /*當 AutoReset 設定為 false 時,Timer 只在第一個 Interval 過後引發一次 Elapsed 事件。 若要保持以 Interval 時間間隔引發 Elapsed 事件,請將 AutoReset 設定為 true。*/ timer.AutoReset = false; timer.Elapsed += new ElapsedEventHandler(OnTimerEvent);//當timer.Start()時,觸發事件 decimal total = 0; // raw test decimal count1 = SingleThreadTest();//單一線程,一跑到底 Console.WriteLine("Single thread count = " + count1.ToString()); // create one thread, increment counter, destroy thread, repeat Console.WriteLine(); CreateAndDestroyTest();//建立一個線程,運算,然後銷毀該線程 重複前面的動作 Console.WriteLine("Create and destroy per count = " + count2.ToString()); // Create 10 threads and run them simultaneously //一次性建立10個線程,然後遍曆使線程執行運算 Console.WriteLine(); InitThreadPoolCounters(); InitThreads(); StartThreads(); while (threadDone != 10) { }; Console.WriteLine("10 simultaneous threads:"); for (int i = 0; i < 10; i++) { Console.WriteLine("T" + i.ToString() + " = " + threadPoolCounters[i].ToString() + " "); total += threadPoolCounters[i]; } Console.WriteLine("Total = " + total.ToString()); Console.WriteLine(); Console.WriteLine("///////////////////////////////////////////////////"); // using ThreadPool //直接通過線程池的QueueUserWorkItem方法,按隊列執行10個任務 Console.WriteLine(); Console.WriteLine("ThreadPool:"); InitThreadPoolCounters(); QueueThreadPoolThreads(); while (threadDone != 10) { }; Console.WriteLine("ThreadPool: 10 simultaneous threads:"); total = 0; for (int i = 0; i < 10; i++) { //threadTimers[i].Stop(); //threadTimers[i].Dispose(); Console.WriteLine("T" + i.ToString() + " = " + threadPoolCounters[i].ToString() + " "); total += threadPoolCounters[i]; } Console.WriteLine("Total = " + total.ToString()); // using SmartThreadPool //通過Amir Bar的SmartThreadPool線程池,利用QueueUserWorkItem方法,按隊列執行10個任務 Console.WriteLine(); Console.WriteLine("SmartThreadPool:"); InitThreadPoolCounters(); QueueSmartThreadPoolThreads(); while (threadDone != 10) { }; Console.WriteLine("SmartThreadPool: 10 simultaneous threads:"); total = 0; for (int i = 0; i < 10; i++) { Console.WriteLine("T" + i.ToString() + " = " + threadPoolCounters[i].ToString() + " "); total += threadPoolCounters[i]; } Console.WriteLine("Total = " + total.ToString()); // using ManagedThreadPool //通過Stephen Toub改進後的線程池,利用QueueUserWorkItem方法,按隊列執行10個任務 Console.WriteLine(); Console.WriteLine("ManagedThreadPool:"); InitThreadPoolCounters(); QueueManagedThreadPoolThreads(); while (threadDone != 10) { }; Console.WriteLine("ManagedThreadPool: 10 simultaneous threads:"); total = 0; for (int i = 0; i < 10; i++) { Console.WriteLine("T" + i.ToString() + " = " + threadPoolCounters[i].ToString() + " "); total += threadPoolCounters[i]; } Console.WriteLine("Total = " + total.ToString()); // using C#4.0 Parallel //通過Tasks.Parallel.For進行並行運算 Console.WriteLine(); Console.WriteLine("Parallel:"); InitThreadPoolCounters(); UseParallelTasks(); while (threadDone != 10) { }; Console.WriteLine("Parallel: 10 simultaneous threads:"); total = 0; for (int i = 0; i < 10; i++) { Console.WriteLine("T" + i.ToString() + " = " + threadPoolCounters[i].ToString() + " "); total += threadPoolCounters[i]; } Console.WriteLine("Total = " + total.ToString()); }
我們可以先熟悉一下大致思路。代碼中,我們主要依靠輸出的數字count或者total來判斷哪個方法執行效率更高(原文是How Hign Can I Count?),通常輸出的數字越大,我們就認為它”乾的活越多“,效率越高。主要實現過程就是通過一個靜態System.Timers.Timer對象的timer執行個體,設定它的Interval屬性和ElapsedEventHandler事件:
static System.Timers.Timer timer = new System.Timers.Timer(1000);/*當 AutoReset 設定為 false 時,Timer 只在第一個 Interval 過後引發一次 Elapsed 事件。 若要保持以 Interval 時間間隔引發 Elapsed 事件,請將 AutoReset 設定為 true。*/timer.AutoReset = false;timer.Elapsed += new ElapsedEventHandler(OnTimerEvent);//當timer.Start()時,觸發事件
其中,timer的事件觸發的函數:
static void OnTimerEvent(object src, ElapsedEventArgs e) { done = true; }
每次timer.Start執行的時候,一次測試就將開始,這樣可以確保測試的不同方法都在1000毫秒內跑完。
下面開始具體介紹幾個方法:
A、線程
這個非常簡單,就是通過主線程計算在1000毫秒內,count從0遞增加到了多少:
/// <summary> /// 單一線程,一跑到底 /// </summary> /// <returns></returns> static decimal SingleThreadTest() { done = false; decimal counter = 0; timer.Start(); while (!done) { ++counter; } return counter; }
while判斷可以保證方法在1000毫秒內執行完成。
B、多線程
這個多線程方法比較折騰,先建立線程,然後運行,最後銷毀線程,這就是一個線程執行單元,重複10次這個線程執行單元。
/// <summary> /// 建立一個線程,運算,然後銷毀該線程 重複前面的動作 /// </summary> static void CreateAndDestroyTest() { done = false; timer.Start(); while (!done) { Thread counterThread = new Thread(new ThreadStart(Count1Thread)); counterThread.IsBackground = true;//後台線程 counterThread.Start(); while (counterThread.IsAlive) { }; } }
那個ThreadStart委託對應的方法Count1Thread如下:
static void Count1Thread() { ++count2; //靜態欄位count2自增 }
從表面上看,大家估計都可以猜到,效果可能不佳。
C、還是多線程
這個方法不判斷線程的執行狀態,不用等到一個線程銷毀後再建立一個線程,然後執行線程方法。線程執行的方法就是根據線程的Name找到一個指定數組的某一索引,並累加改變數組的值:
/// <summary> /// 將數組和線程數標誌threadDone回到初始狀態 /// </summary> static void InitThreadPoolCounters() { threadDone = 0; for (int i = 0; i < 10; i++) { threadPoolCounters[i] = 0; } } /// <summary> /// 初始化10個線程 /// </summary> static void InitThreads() { for (int i = 0; i < 10; i++) { threads[i] = new Thread(new ThreadStart(Count2Thread)); threads[i].IsBackground = true; threads[i].Name = i.ToString();//將當前線程的Name賦值為數組索引,在Count2Thread方法中擷取對應數組 } } /// <summary> /// 開始多線程運算 /// </summary> static void StartThreads() { done = false; timer.Start(); for (int i = 0; i < 10; i++) { threads[i].Start(); } }
其中,每一個線程需要執行的委託方法
static void Count2Thread() { int n = Convert.ToInt32(Thread.CurrentThread.Name);//取數組索引 while (!done) { ++threadPoolCounters[n]; } Interlocked.Increment(ref threadDone);//以原子操作的形式保證threadDone遞增 }
在測試過程中,我們看代碼:
// Create 10 threads and run them simultaneously //一次性建立10個線程,然後遍曆使線程執行運算 Console.WriteLine(); InitThreadPoolCounters(); InitThreads(); StartThreads(); while (threadDone != 10) { }; Console.WriteLine("10 simultaneous threads:"); for (int i = 0; i < 10; i++) { Console.WriteLine("T" + i.ToString() + " = " + threadPoolCounters[i].ToString() + " "); total += threadPoolCounters[i]; } Console.WriteLine("Total = " + total.ToString()); Console.WriteLine();
最後算出這個數組的所有元素的總和,就是這10個線程在1000毫秒內所做的事情。其中, while (threadDone != 10) { };這個判斷非常重要。這個方法看上去沒心沒肺,線程建立好就不管它的死活了(還是管活不管死?),所以效率應該不低。
實際上,我在本地測試並看了一下輸出,表面看來,按count大小逆序排列:C>A>B,這就說明多線程並不一定比單線程運行效率高。其實B之所以效率不佳,主要是由於這個方法大部分的”精力“花線上程的執行狀態和銷毀處理上。
注意,其實C和A、B都沒有可比性,因為C計算的是數組的總和,而A和B只是簡單的對一個數字進行自加。
ps:C這一塊說的沒有中心,想到哪寫到哪,所以看起來寫得很亂,如果看到這裡您還覺著不知所云,建議先下載最後的demo,先看代碼,再對照這篇文章。
好了,到這裡,我們對線程的建立和使用應該有了初步的瞭解。細心的人可能會發現,我們new一個Thread,然後給線程執行個體設定屬性,比如是否後台線程等等,其實這部分工作可以交給下面介紹的線程池ThreadPool來做(D、E和F主要介紹線程池)。
D、線程池ThreadPool
在實際的項目中大家可能使用最多最熟悉的就是這個類了,所以沒什麼可說的:
/// <summary> /// ThreadPool測試 /// </summary> static void QueueThreadPoolThreads() { done = false; for (int i = 0; i < 10; i++) { ThreadPool.QueueUserWorkItem(new WaitCallback(Count3Thread), i); } timer.Start(); } static void Count3Thread(object state) { int n = (int)state; while (!done) { ++threadPoolCounters[n]; } Interlocked.Increment(ref threadDone); }
我們知道線程池裡的線程預設都是後台線程,所以它實際上簡化了線程的屬性設定,更方便非同步編程。
需要說明的是,線程池使用過程中會有這樣那樣的缺陷(雖然本文的幾個線程池任務都不會受這種缺陷影響)。比如,我們一次性向線程池中加入100個任務,但是當前的系統可能只支援25個線程,並且每個線程正處於”忙碌“狀態,如果一次性加入池中系統會處理不過來,那麼多餘的任務必須等待,這就造成等待的時間過長,系統無法響應。還好,ThreadPool提供了GetAvailableThreads方法,可以讓你知道當前可用的背景工作執行緒數量。
static void QueueThreadPoolThreads() { done = false; for (int i = 0; i < 10; i++) { //ThreadPool.QueueUserWorkItem(new WaitCallback(Count3Thread), i); //直接給程式池新增工作有時是很草率的 WaitCallback wcb = new WaitCallback(Count3Thread); int workerThreads, availabeThreads; ThreadPool.GetAvailableThreads(out workerThreads, out availabeThreads); if (workerThreads > 0)//可用線程數>0 { ThreadPool.QueueUserWorkItem(wcb, i); } else { //to do 可以採取一種策略,讓這個任務合理地分配給線程 } }
如果沒有可用的背景工作執行緒數,必須設計一定的策略,讓這個任務合理地分配給線程。
也許就是類似於上面那樣的限制,很多開發人員都自己建立自己的線程池,同時也就有了後面的SmartThreadPool和ManagedThreadPool大展身手的機會。
E、線程池SmartThreadPool
大名鼎鼎的SmartThreadPool,但是我從來沒在項目中使用過,所以只是找了一段簡單的代碼測試一下:
/// <summary> /// SmartThreadPool測試 /// </summary> static void QueueSmartThreadPoolThreads() { SmartThreadPool smartThreadPool = new SmartThreadPool(); // Create a work items group that processes // one work item at a time IWorkItemsGroup wig = smartThreadPool.CreateWorkItemsGroup(1); done = false; timer.Start(); for (int i = 0; i < 10; i++) { wig.QueueWorkItem(new WorkItemCallback(Count4Thread), i); } // Wait for the completion of all work items in the work items group wig.WaitForIdle(); smartThreadPool.Shutdown(); } static object Count4Thread(object state) { int n = (int)state; while (!done) { ++threadPoolCounters[n]; } Interlocked.Increment(ref threadDone); return null; }
自從收藏這個SmartThreadPool.dll後,我還從沒有在項目中使用過。查看它的源碼注釋挺少也挺亂的,不知道有沒有高人知道它的一個效率更好的方法。您也可以看看英文原文,自己嘗試體驗一下。如果您熟悉使用SmartThreadPool,歡迎討論。
F、線程池ManagedThreadPool
Stephen Toub這個完全用C#Managed 程式碼實現的線程池也非常有名,在Marc Clifton的英文原文中,作者也不吝溢美之詞,贊它“quite excellent”,用當前異軍突起的一個詞彙形容就是太給力了,於我心有戚戚焉:
/// <summary> /// ManagedThreadPool測試 /// </summary> static void QueueManagedThreadPoolThreads() { done = false; timer.Start(); for (int i = 0; i < 10; i++) { Toub.Threading.ManagedThreadPool.QueueUserWorkItem(new WaitCallback(Count5Thread), i); } } static void Count5Thread(object state) { int n = (int)state; while (!done) { ++threadPoolCounters[n]; } Interlocked.Increment(ref threadDone); }
對於這個託管的線程池,我個人的理解,就是它在管理線程的時候,這個池裡還有一個緩衝線程的池,即一個ArrayList對象。它一開始就初始化了一定數量的線程,並通過ProcessQueuedItems方法保證非同步執行進入池中的隊列任務(那個死迴圈有時可能導致CPU過分忙碌),這樣在分配非同步任務的時候,就省去了頻繁去建立(new)一個線程。同時它在實現訊號量(Semaphore)的同步和線程出入隊列的設計上都可圈可點,非常巧妙,強烈推薦您閱讀它的源碼。
G、並行運算
下面的樣本,我只使用了簡單的System.Threading.Tasks.Parallel.For 對應的for 迴圈的並行運算:
/// <summary> /// 並行運算測試 /// </summary> static void UseParallelTasks() { done = false; timer.Start(); // System.Threading.Tasks.Parallel.For - for 迴圈的並行運算 System.Threading.Tasks.Parallel.For(0, 10, (i) => { Count6Thread(i); }); } static void Count6Thread(object state) { int n = (int)state; while (!done) { ++threadPoolCounters[n]; } Interlocked.Increment(ref threadDone); }
沒有什麼要特殊說明的,就是新類庫的使用。看代碼,好像比使用線程或線程池更加簡單直接,有機會爭取多用一用。我在本地測試的時候,在Release版本下,按照count的大小逆序排列,總體上G>D>F>E。需要注意到一件事,就是SmartThreadPool中排入隊列的任務是一個傳回值為Object的委託類型,這和其他的幾個沒有返回的(void類型)不同。SmartThreadPool口碑還是不錯的,也許是我沒有正確使用它。
最後小結一下:本文主要列舉了C#中我所知道的幾種常見的非同步處理的方法,歡迎大家錯誤修正或補充。
樣本下載:demo