淺談執行緒池(下):相關試驗及注意事項
三個月,整整三個月了,我忽然發現我還有三個月前的一個小系列的文章沒有結束,我還欠一個試驗!執行緒池是.NET中的重要元件,幾乎所有的非同步功能依賴於執行緒池。之前我們討論了執行緒池的作用、獨立執行緒池的存在意義,以及對CLR執行緒池和IO執行緒池進行了一定說明。不過這些說明可能有些“抽象”,於是我們還是要通過試驗來“驗證”這些說明。此外,我認為針對某個“猜想”來設計一些試驗進行驗證是非常重要的能力,如果您這方面的能力略有不足的話,還是儘量加以鍛鍊並提高吧。
CLR執行緒的使用與建立
首先,我們準備這樣一段程式碼:
public static void ThreadUseAndConstruction() { ThreadPool.SetMinThreads(5, 5); // set min thread to 5 ThreadPool.SetMaxThreads(12, 12); // set max thread to 12 Stopwatch 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秒鐘。那麼請您思考一下,這段程式碼的輸出是什麼樣的呢?
展開
摺疊 00:00:00.0028309: Task 0 started 00:00:00.0079552: Task 1 started 00:00:00.0080033: Task 2 started 00:00:00.0081628: Task 3 started 00:00:01.0058442: Task 4 started 00:00:01.5039911: Task 5 started 00:00:02.0048392: Task 6 started 00:00:02.5051786: Task 7 started 00:00:03.0051154: Task 8 started 00:00:04.0048998: Task 9 started 00:00:05.0053109: Task 10 started 00:00:06.0068503: Task 11 started 00:00:10.0079897: Task 1 finished 00:00:10.0084587: Task 12 started 00:00:10.0087316: Task 2 finished 00:00:10.0079939: Task 3 finished 00:00:10.0090849: Task 14 started 00:00:10.0088292: Task 0 finished 00:00:10.0101870: Task 15 started 00:00:10.0089327: Task 13 started 00:00:11.0059534: Task 4 finished 00:00:11.0063235: Task 16 started 00:00:11.5040658: Task 5 finished 00:00:11.5044820: Task 17 started 00:00:12.0051271: Task 6 finished 00:00:12.0064219: Task 18 started 00:00:12.5061198: Task 7 finished 00:00:12.5074655: Task 19 started 00:00:13.0052512: Task 8 finished 00:00:14.0052185: Task 9 finished 00:00:15.0064023: Task 10 finished 00:00:16.0074270: Task 11 finished 00:00:20.0085632: Task 12 finished 00:00:20.0094829: Task 14 finished 00:00:20.0104810: Task 13 finished 00:00:20.0119368: Task 15 finished 00:00:21.0066450: Task 16 finished 00:00:21.5045454: Task 17 finished 00:00:22.0116638: Task 18 finished 00:00:22.5107089: Task 19 finished
高位的零我們就直接忽略了,我們只觀察“秒”及以下精度的時間。對這個資料進行簡單觀察之後,我們發現可以把時間精確到0.5秒來描述每個時刻所發生的事情:
- 0秒:任務0至任務3,共計4個任務開始執行。
- 1至3秒:任務4至任務8依次執行,間隔為0.5秒。
- 3至6秒:任務8至任務11依次執行,間隔為1秒。
- 10秒:任務0至任務3執行完成,任務12至任務15開始執行。
- 11至12.5秒:每執行完一箇舊任務(4至7),便立即開始一個新任務(16至19)。
- 13至22.5秒:剩餘任務(8至19)依次結束。
您猜對了嗎?我沒有猜對,因為有兩點:
- 原來最小執行緒數量為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請求,觀察一下它們的回撥函式執行時刻。這些不如就由您來自行完成了?
原文地址:http://www.cnblogs.com/JeffreyZhao/archive/2009/10/20/thread-pool-3-lab.html
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/12639172/viewspace-617200/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 淺談執行緒池(上):執行緒池的作用及CLR執行緒池執行緒
- 淺談執行緒池(中):獨立執行緒池的作用及IO執行緒池執行緒
- 執行緒池相關執行緒
- 執行緒池相關複習執行緒
- 多執行緒CreateThread函式的用法及注意事項執行緒thread函式
- Qt中的多執行緒與執行緒池淺析+例項QT執行緒
- java執行緒池趣味事:這不是執行緒池Java執行緒
- openGauss執行緒池相關引數執行緒
- 關於mysql執行效率優化注意事項及要點MySql優化
- 執行緒的建立及執行緒池執行緒
- 多執行緒的執行緒狀態及相關操作執行緒
- JAVA多執行緒使用場景和注意事項Java執行緒
- 多執行緒-NSOperation中使用ASIHttpRequest注意事項執行緒HTTP
- 執行緒與執行緒池的那些事之執行緒池篇(萬字長文)執行緒
- 淺談多執行緒執行緒
- 淺談JS執行緒JS執行緒
- 淺談 iOS 執行緒iOS執行緒
- 執行緒概念淺談執行緒
- 安裝python3.5注意事項及相關命令Python
- 深入淺出Java多執行緒(十二):執行緒池Java執行緒
- 深入淺出執行緒池+高階選項的使用執行緒
- 案例分析|執行緒池相關故障梳理&總結執行緒
- ]淺談幾種伺服器端模型——多執行緒併發式(執行緒池)伺服器模型執行緒
- JAVA-執行緒池淺析Java執行緒
- 深入淺出 Java 執行緒池Java執行緒
- 執行緒池核心原理淺析執行緒
- 程式池、執行緒池效率測試執行緒
- 【雜談】JS相關的執行緒模型整理JS執行緒模型
- Java執行緒池的那些事Java執行緒
- 淺談 Java多執行緒Java執行緒
- 淺談 Java執行緒狀態轉換及控制Java執行緒
- Python執行緒專題8:使用鎖的注意事項Python執行緒
- 淺析Java中的執行緒池Java執行緒
- 多執行緒之間通訊及執行緒池執行緒
- 詳解執行緒池的作用及Java中如何使用執行緒池執行緒Java
- Java執行緒池原理及分析Java執行緒
- 淺談linux執行緒模型和執行緒切換Linux執行緒模型
- Java執行緒池二:執行緒池原理Java執行緒