浅谈线程池(下):相关试验及注意事项
三個月,整整三個月了,我忽然發現我還有三個月前的一個小系列的文章沒有結束,我還欠一個試驗!線程池是.NET中的重要組件,幾乎所有的異步功能依賴于線程池。之前我們討論了線程池的作用、獨立線程池的存在意義,以及對CLR線程池和IO線程池進行了一定說明。不過這些說明可能有些“抽象”,于是我們還是要通過試驗來“驗證”這些說明。此外,我認為針對某個“猜想”來設計一些試驗進行驗證是非常重要的能力,如果您這方面的能力略有不足的話,還是盡量加以鍛煉并提高吧。
CLR線程的使用與創建
首先,我們準備這樣一段代碼:
public static void ThreadUseAndConstruction() {ThreadPool.SetMinThreads(5, 5); // set min thread to 5ThreadPool.SetMaxThreads(12, 12); // set max thread to 12Stopwatch watch = new Stopwatch();watch.Start();WaitCallback callback = index =>{Console.WriteLine(String.Format("{0}: Task {1} started", watch.Elapsed, index));Thread.Sleep(10000);Console.WriteLine(String.Format("{0}: Task {1} finished", watch.Elapsed, index));};for (int i = 0; i < 20; i++){ThreadPool.QueueUserWorkItem(callback, i);} }這段代碼很簡單。首先將線程池最小和最大線程數量設為5和12,然后向線程池中連續推入20個任務,每個任務都是打印出執行時的當前時間,然后等待10秒鐘。那么請您思考一下,這段代碼的輸出是什么樣的呢?
展開高位的零我們就直接忽略了,我們只觀察“秒”及以下精度的時間。對這個數據進行簡單觀察之后,我們發現可以把時間精確到0.5秒來描述每個時刻所發生的事情:
您猜對了嗎?我沒有猜對,因為有兩點:
- 原來最小線程數量為5時,只有4個線程可以立即執行。經過進一步嘗試,最小線程數量為10時,也只有9個線程可以立即執行。
- 原來線程池創建線程的速度并非永遠是“每秒2個”,而一些資料上寫著“每秒不超過2個”的確是確切的說法。
但是,我們還是驗證了以下幾個結論:
- 在線程池最小線程數量的范圍之內,盡可能多的任務立即執行。
- 線程池使用使用每秒不超過2個的頻率創建線程(1秒一個或0.5秒一個)。
- 當達到線程池最大線程數時(第6秒),停止創建新線程。
- 在舊任務執行完畢后,新任務立即執行。
當然,由于我們在這之前已經“了解”了線程池是如何工作的,因此這里得到的結果可能會有“自圓其說”的傾向在里面。要減少這個可能性,則需要設計更完整的試驗來“解釋”問題。您也可以順著這一點進行更深入的探索。
線程池中的線程是“公用”的
我們沒有獨立創建線程,而是選擇使用線程池一定有其原因。不過,我們既然使用了線程池,就有一些額外的東西值得注意。
首先,我們要明確一個觀念:線程并不“屬于”任何一個任務,或者說任務并不“擁有”線程。我們只是借用一個線程來做事,用完以后便會還回。也就是說,任務在執行時修改線程的信息(名稱,優先級,語言文化等等)是沒有意義的,此外,任務也不應該依賴線程的這些狀態。還記得上篇文章中談到的QueueUserWorkItem和UnsafeQueueUserWorkItem之間的區別嗎?如果您的任務需要依賴什么東西,也請自行準備。線程池中的線程狀態是不可靠的。當然,也盡量不要直接對當前線程進行其他操作。
其次,由于線程池有大小限制,在某些時候還可能出現死鎖的情況:
static void WaitCallback(object handle) {ManualResetEvent waitHandle = (ManualResetEvent)handle;for (int i = 0; i < 10; i++){ThreadPool.QueueUserWorkItem(state =>{int index = (int)state;if (index == 9){waitHandle.Set(); // release all }else{waitHandle.WaitOne(); // wait }}, i);} }public static void DeadLock() {ManualResetEvent waitHandle = new ManualResetEvent(false);ThreadPool.SetMaxThreads(5, 5);ThreadPool.QueueUserWorkItem(WaitCallback, waitHandle);waitHandle.WaitOne(); }在上面的代碼中,waitHandle將永遠阻塞。因為我們放入線程池的10個任務,只有最后一個會將waitHandle打開,其余任務也統統阻塞在這個waitHandle上。但是請注意,我們使用SetMaxThreads方法把最大線程數限制為5,這樣第10個任務根本無法執行,從而進入了死鎖。避免這個問題最簡單的做法是增加最大線程數,但是這還是會產生許多無法工作的線程,造成資源的浪費。因此,最好的做法是重新設計并行算法,并且時刻記住:“不要阻塞線程池里的線程”。
如何合理而有效的使用線程(既不多也不少還不阻塞),這是并行算法中最常見的課題之一。例如,讓您設計一個并行計算斐波那契數列的算法,如果您每次計算Fib(n)時,都創建兩個新的任務來并行計算Fib(n - 1)和Fib(n - 2),并等待它們結束,就會造成上述的死鎖(或大量線程)。如何解決這個問題?您可以觀察一下.NET 4.0中新增的Task并行類庫,它提供了豐富而易用的并行運算API,幫我們省去了大量的工作1。
最后,便是時刻記得系統中哪些功能依賴線程池。例如ASP.NET中的請求也會使用CLR線程池,那么您是否應該使用ThreadPool?是否應該直接使用委托的異步調用?您是否應該調整線程池的最大和最小線數?這些問題沒有確定答案,這需要您根據實際情況自己做判斷。
CLR線程池與IO線程池
當第一次了解到.NET準備了一個CLR線程池和一個IO線程池的時后,我在想,這兩者真的是沒有關系的嗎?他們會互相影響嗎?于是我做了這么一個試驗:
public static void IoThread() {ThreadPool.SetMinThreads(5, 3);ThreadPool.SetMaxThreads(5, 3);ManualResetEvent waitHandle = new ManualResetEvent(false);Stopwatch watch = new Stopwatch();watch.Start();WebRequest request = HttpWebRequest.Create("http://www.cnblogs.com/");request.BeginGetResponse(ar =>{var response = request.EndGetResponse(ar);Console.WriteLine(watch.Elapsed + ": Response Get");}, null);for (int i = 0; i < 10; i++){ThreadPool.QueueUserWorkItem(index =>{Console.WriteLine(String.Format("{0}: Task {1} started", watch.Elapsed, index));waitHandle.WaitOne();}, i);}waitHandle.WaitOne(); }得到的結果是這樣的:
00:00:00.0923543: Task 0 started 00:00:00.1152495: Task 2 started 00:00:00.1153073: Task 3 started 00:00:00.1152439: Task 1 started 00:00:01.0976629: Task 4 started 00:00:01.5235481: Response Get從中可以看出,我們將CLR線程池的最大線程數量設為了5,并使用與上一例類似的做法故意“阻塞”了線程池(而只有5個任務被執行了,說明線程池的確被阻塞了),其目的便是觀察在這種情況下一個IO異步請求是否能夠得到正確的回復。答案是肯定的,IO異步請求的回調函數正常執行了。這意味著,雖然CLR線程池被用完了,但是似乎的確還是有一個額外的IO線程池在處理IO的異步回調。這樣看來,CLR線程池和IO線程池兩者并沒有影響。此外,從.NET框架所設計的類庫來看,的確將兩者作了區分,例如:
public static class ThreadPool {public static bool GetAvailableThreads(out int workerThreads, out int completionPortThreads); }不過,這并不意味著CLR線程池中線程被用完之后,還是可以發起異步IO請求。例如,您可以嘗試著將這個例子中的WebRequest操作放到for循環后面(確保CLR線程池中線程已經被用完了),這是您會發現BeginGetRequest方法的調用拋出了一個異常,提示您說線程池中沒有多余的線程了。從這個角度這樣看來,CLR線程池的確還是可能影響異步IO操作的(多謝xiongli大哥指出“這是由具體實現決定的”)——雖然這在普通應用程序中一般不會出現這個問題。
其實在IO線程池方面還可以進行其他一些試驗。例如,您可以縮小IO線程池的最大線程數量,然后一下子發起多個異步IO請求,觀察一下它們的回調函數執行時刻。這些不如就由您來自行完成了?
相關文章
- 淺談線程池(上):線程池的作用及CLR線程池
- 淺談線程池(中):獨立線程池的作用及IO線程池
- 淺談線程池(下):相關試驗及注意事項
?
注1:.NET 4.0在多線程方面進行了明顯的增強,除了Task并行類庫之外,也將Parallel Library并入框架之內。此外,.NET 4.0還提供了許多線程安全的并行容器,以及輕量級的CountDownLatch、SemaphoreSlim、SpinWait等常用組件,無論是學習還是使用都是絕佳的范例。
from:?http://blog.zhaojie.me/2009/10/thread-pool-3-lab.html
總結
以上是生活随笔為你收集整理的浅谈线程池(下):相关试验及注意事项的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 浅谈线程池(中):独立线程池的作用及IO
- 下一篇: 浅谈代码的执行效率(1):算法是关键