三個月,整整三個月了,我突然發現我還有三個月前的一個小系列的文章沒有結束,我還欠一個試驗!線程池是.NET中的重要組件,幾乎全部的異步功能依賴於線程池。以前咱們討論了線程池的做用、獨立線程池的存在乎義,以及對CLR線程池和IO線程池進行了必定說明。不過這些說明可能有些「抽象」,因而咱們仍是要經過試驗來「驗證」這些說明。此外,我認爲針對某個「猜測」來設計一些試驗進行驗證是很是重要的能力,若是您這方面的能力略有不足的話,仍是儘可能加以鍛鍊並提升吧。html
首先,咱們準備這樣一段代碼:算法
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秒鐘。那麼請您思考一下,這段代碼的輸出是什麼樣的呢?安全
展開
高位的零咱們就直接忽略了,咱們只觀察「秒」及如下精度的時間。對這個數據進行簡單觀察以後,咱們發現能夠把時間精確到0.5秒來描述每一個時刻所發生的事情:多線程
您猜對了嗎?我沒有猜對,由於有兩點:框架
可是,咱們仍是驗證瞭如下幾個結論:異步
固然,因爲咱們在這以前已經「瞭解」了線程池是如何工做的,所以這裏獲得的結果可能會有「自圓其說」的傾向在裏面。要減小這個可能性,則須要設計更完整的試驗來「解釋」問題。您也能夠順着這一點進行更深刻的探索。函數
咱們沒有獨立建立線程,而是選擇使用線程池必定有其緣由。不過,咱們既然使用了線程池,就有一些額外的東西值得注意。學習
首先,咱們要明確一個觀念:線程並不「屬於」任何一個任務,或者說任務並不「擁有」線程。咱們只是借用一個線程來作事,用完之後便會還回。也就是說,任務在執行時修改線程的信息(名稱,優先級,語言文化等等)是沒有意義的,此外,任務也不該該依賴線程的這些狀態。還記得上篇文章中談到的QueueUserWorkItem和UnsafeQueueUserWorkItem之間的區別嗎?若是您的任務須要依賴什麼東西,也請自行準備。線程池中的線程狀態是不可靠的。固然,也儘可能不要直接對當前線程進行其餘操做。pwa
其次,因爲線程池有大小限制,在某些時候還可能出現死鎖的狀況:線程
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?是否應該直接使用委託的異步調用?您是否應該調整線程池的最大和最小線數?這些問題沒有肯定答案,這須要您根據實際狀況本身作判斷。
當第一次瞭解到.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請求,觀察一下它們的回調函數執行時刻。這些不如就由您來自行完成了?
注1:.NET 4.0在多線程方面進行了明顯的加強,除了Task並行類庫以外,也將Parallel Library併入框架以內。此外,.NET 4.0還提供了許多線程安全的並行容器,以及輕量級的CountDownLatch、SemaphoreSlim、SpinWait等經常使用組件,不管是學習仍是使用都是絕佳的範例。