線程池ThreadPool的初探


一、線程池的適用范圍

  在日常使用多線程開發的時候,一般都構造一個Thread示例,然后調用Start使之執行。如果一個線程它大部分時間花費在等待某個事件響應的發生然后才予以響應;或者如果在一定期間內重復性地大量創建線程。這些時候個人感覺利用線程池(ThreadPool)會比單純創建線程(Thread)要好。這是由於線程池能在需要的時候把空閑的線程提取出來使用,在線程使用完畢的時候對線程回收達到對象復用的效果。這個就涉及到池的性質了。線程(Thread)很容易跟數據庫連接、流、Socket套接字這部分非托管資源歸在一起,但是個人認為Thread並不是非托管資源,有個低級點的判別辦法,就是Thread沒有去實現IDispose接口,利用Reflector打開去查看的話,里面就有一個析構函數~Thread()它實際上是調用了一個外部方法InternalFinalize(),估計這個就涉及到CLR里面的東西了。如果頻繁開啟線程,對資源的消耗會比用線程池的要多。

二、池的容量和對象管理

  既然上面提及到池的性質,在TheardPool這個線程池中也可以看到一個對象池的特點,這個可以在日后我們創建對象池時可以作為參考。雖然本人以前也寫過一個Socket的對象池,但是運行起來的性能不好。現在個人不清楚在CLR中是否本身存在一個Socket的對象池,但看了老趙的博客發現CLR內部其實擁有一個數據庫連接的對象池,實現的效果跟ThreadPool類似,能讓對象復用。

  在以前定義Socket池時只定義了一個對象上限,沒有下限的概念;在ThreadPool中,池內對象的上下限都可以進行設置和獲取

1 public static bool SetMinThreads(int workerThreads, int completionPortThreads);
2 public static bool SetMaxThreads(int workerThreads, int completionPortThreads);
3 
4 public static void GetMaxThreads(out int workerThreads, out int completionPortThreads);
5 public static void GetMinThreads(out int workerThreads, out int completionPortThreads);

  至於這里有兩種線程的原因遲點再提。MinThread指的是線程池初始或者空閑時保留最少的線程數,這個值與CLR的版本和CPU的核心數有關系。在CLR SP1之前的版本中,線程池默認最大線程數是  處理器數 * 25,在CLR SP1之后默認最大線程數是 處理器數 * 250。最少線程數則是 處理器數,於是我也嘗試了一下。不過這里又涉及到CLR與.NET Framework的關系。

.NET Framework | CLR
---------------------------------------
2.0 RTM      | 2.0.50727.42
2.0 SP1      | 2.0.50727.1433
2.0 SP2      | 2.0.50727.3053
3.0 RTM      | 2.0 RTM
3.0 SP1     | 2.0 SP1
3.0 SP2     | 2.0 SP2
3.5 RTM      | 2.0 SP1
3.5 SP1      | 2.0 SP2
4.0 RTM      | 4.0.30319.1

我自己通過 Environment類的Version屬性獲取CLR的版本號。下面這段代碼,我使用幾個版本的.NET Framework去編譯 。

            int i1,i2;
            ThreadPool.GetMaxThreads(out i1, out i2);
            Console.WriteLine("Max workerThreads :"+ i1+"  completionPortThreads:"+i2);
            ThreadPool.GetMinThreads(out i1,out i2);
            Console.WriteLine("Min workerThreads:"+i1 + "  completionPortThreads:" + i2);


            Console.WriteLine("     CLR Version: {0}  ", Environment.Version);

得出的結果有點失望,失望的不是與上面說的相違背。而是我這里用的.NET Framework不全。

2.0和3.5的CLR都是SP2本版本的

3.5的結果如下

2.0的結果如下

從上面的結果看出最大線程數和最小線程數符合。還是得說一下我用的是i5處理器,雙核四線程。

下面這個我是在虛擬機上跑的,單核的虛擬機

 

用的是.NET Framework1.0的,CLR也是1.0的。的確最少線程數和最多工作線程數是對得上的,但是IO線程數還是保留着1000個。最后看看上跑熟悉的.NET 4.0的

我在虛擬機和本機上分別跑過,IO線程還是一樣1000沒變,估計前面的公式對它不適用,但工作數還是有點怪怪的,單核的就1023條,但是在i5上的卻不是1024的倍數。

  使用了線程池這個對象,給人的感覺就不像是往常使用其他對象的那種方式——調用,而是類似於Web服務器的請求與響應的方式。這個理念跟我設計的Socket池有點不一樣。說回線程池里面對線程的管理情況,在沒有對線程池提交過任何任務請求的時候,線程池內真正開創的線程數可並不是那么多,實際上僅僅是小於等於最小的線程數。參照了老趙的代碼

 1             int maxCount = 18;
 2             int minCount = 16;
 3             ThreadPool.SetMaxThreads(maxCount, maxCount);
 4             ThreadPool.SetMinThreads(minCount, minCount);
 5 
 6             Stopwatch watch = new Stopwatch();
 7             watch.Start();
 8 
 9             WaitCallback callback = i =>
10             {
11                 Console.WriteLine(String.Format("{0}: Task {1} started", watch.Elapsed, i));
12                 Thread.Sleep(10000);
13                 Console.WriteLine(String.Format("{0}: Task {1} finished", watch.Elapsed, i));
14             };
15 
16             for (int i = 0; i < 20; i++)
17             {
18                 ThreadPool.QueueUserWorkItem(callback, i);
19             }

運行結果如下

  從上圖可以看出,當一開始請求任務的時候,線程池能馬上響應去處理任務,16條信息都能在一秒內完成,而這個16則是剛與最小線程數相等。而老趙的博客上說一秒內創建的線程數會小於最小線程數。估計是我現在用的處理器性能還可以吧。不過我也在單核的虛擬機上運行,同樣也是一秒內創建的線程數跟最小線程數相等。但同時我也發現了另一個情況,就是在真實的電腦上運行上述代碼,把最小線程數設成小於4的,同樣一開始也能同時創建了4條線程,個人估計這個跟具有雙核四線程的i5CPU有很大關系,在虛擬機上運行就沒這情況了。

  既然初始創建的線程數並非是最大線程數,而是在線程池使用過程中遇到線程不夠用了才去創建新線程,直到達到最大值為止,這樣的設計大大節省了對資源的占用。同時也引發了另一個問題,線程的創建速度,這個創建速度會影響到響應請求的時間。每次請求肯定希望盡快得到響應,但是如果響應的速度過快,萬一在一瞬間有大量簡短的任務涌入線程池,任務完畢后對已經用完的線程進行回收也是一個比較大的開銷。所以這個線程的創建速度也是得講究的。看了並運行過老趙的代碼,的確發現1秒內會創建了兩個線程,但絕大部分是1秒只創建一個。我自己稍作改動,讓結果更清晰些

 1             Dictionary<int, TimeSpan> createTime = new Dictionary<int, TimeSpan>();
 2             int maxCount = 12;
 3             int minCount = 5;
 4             ThreadPool.SetMaxThreads(maxCount, maxCount);
 5             ThreadPool.SetMinThreads(minCount, minCount);
 6 
 7             Stopwatch watch = new Stopwatch();
 8             watch.Start();
 9             
10             WaitCallback callback = i =>
11             {
12                 lock (this)
13                 {
14                     TimeSpan ts = watch.Elapsed;
15                     if (!createTime.ContainsKey(Thread.CurrentThread.ManagedThreadId))
16                     {
17                         createTime[Thread.CurrentThread.ManagedThreadId] = ts;
18                         Console.WriteLine("{0} {1} {2}", Thread.CurrentThread.ManagedThreadId, ts, i);
19                     }
20                 }
21                 Thread.Sleep(10000);
22             };
23 
24             for (int i = 0; i < 20; i++)
25             {
26                 ThreadPool.QueueUserWorkItem(callback, i);
27             }
28     

同樣運行老趙的代碼也不一定能看到每秒創建兩個線程,我段代碼貌似更難以看見了,估計是因為有了鎖的原因。

這個結果我試了很多回才弄了出來,好像例子很生硬,但1秒一個線程還是很明顯能看出來的。 

三、池內對象分類

  在提及獲取和設置線程池上下限的部分提及過,一個線程池內有兩種類型的線程,一種是工作線程,另一種是IO線程。兩種線程其使用時會有差異,在向線程池發出任務請求的時候,即調用QueueUserWorkItem或者UnsafeQueueUserWorkItem方法時。使用的線程是工作線程的線程。在使用APM模式時,有部分是使用了工作線程,有部分是使用了IO線程。這里大部分都是使用了工作線程,只有少部分會使用IO線程。在使用真正的異步方法回調時才會使用IO線程,哪些類的BeginXXX/EndXX方法會真正地用上異步,在鄙人上一篇博文中提到。不過本人閱讀了老趙的博客反復試驗之后得出了一個結果,即使是FileStream,Dns,Socket,WebRequest,SqlCommanddeng的異步操作,它們也會調用到線程池里面的線程。在不同的階段調用了不同的線程。那么先看一下下面的代碼,要注意一下的是,本人發現如果要把線程池的上下限設成同一個值的話,那只能先設下限再設上限,否則上限會恢復到默認值的。

 1             ThreadPool.SetMinThreads(5, 3);
 2             ThreadPool.SetMaxThreads(5, 3);
 3             ManualResetEvent waitHandle = new ManualResetEvent(false);
 4 
 5             for (int i = 0; i < 5; i++)
 6             {
 7                 FileStream fs = new FileStream("test" + i + ".txt", FileMode.Create, FileAccess.Write, FileShare.Write, 1024, FileOptions.Asynchronous);
 8                 string content = "hello world";
 9                 byte[] arr = Encoding.Default.GetBytes(content);
10 
11                 fs.BeginWrite(arr, 0, arr.Length, (asyncPara) =>
12                 {
13                     FileStream caller = asyncPara.AsyncState as FileStream;
14                     caller.EndWrite(asyncPara);
15                     caller.Close();
16                     caller.Dispose();
17                     int workC, ioC;
18                     ThreadPool.GetAvailableThreads(out workC, out ioC);
19                     Console.WriteLine(String.Format("Write Finish work {0} io {1}", workC, ioC));
20                     waitHandle.WaitOne();
21                 }, fs);
22             }

  這里運用到了線程池ThreadPool的GetAvailableThreads方法,方法的描述是獲取線程池最大線程數和當前使用線程數的差值,個人認為就是獲取線程池的空閑線程數。在前面的文章中已經提及到,異步方法回調時會開辟線程回調方法,而這條開辟的線程是來自於線程池的,這段代碼中只調用了FileStream類的異步方法,並沒有其他調用線程池的方法。看看運行結果

  可以明顯地看出,在異步寫文件的操作中,工作線程有被使用,IO線程也有被使用,這個結果跟我之前猜測的有出入。原本以為進行異步操作時,調用了Begin方法則是利用了系統API去讓設備直接訪存,在DMA結束之后開辟了一條IO線程去進行方法的回調。但是看了這個情況之后,本人就認為,在調用了Begin方法之后,線程池使用了一條IO線程去調用系統的API讓設備訪存,結束后使用的線程卻是一條工作線程。假如這時候工作線程已經用完了,那么對於調用“真異步”或者“假異步”的線程來說都會造成阻塞。當然這里用了回調函數,那么不用回調函數的結果會怎么樣。把工作線程設成只有一條。

 1             ThreadPool.SetMinThreads(1, 5);
 2             ThreadPool.SetMaxThreads(1, 5);
 3 
 4             ManualResetEvent waitHandle = new ManualResetEvent(false);
 5             for (int i = 0; i < 1; i++)
 6                 ThreadPool.QueueUserWorkItem((para) =>
 7                 {
 8                     waitHandle.WaitOne();
 9                 });
10             string content = "hello world";
11             content += "end";
12             byte[] arr = Encoding.Default.GetBytes(content);
13             
14             for (int i = 0; i < 6; i++)
15             {
16                 FileStream fs = new FileStream("test" + i + ".txt", FileMode.Create, FileAccess.Write, FileShare.Write, 1024, FileOptions.Asynchronous);
17 
18                 IAsyncResult result = fs.BeginWrite(arr, 0, arr.Length, null, null);
19 
20                 fs.EndWrite(result);
21                 fs.Close();
22                 fs.Dispose();
23

  文件照樣能正常輸出,沒有因工作線程已經用完而影響。但IO線程與工作線程兩者間並非沒有聯系,在老趙的博客中看到,如果工作線程已經用完,而調用WebRequest的BeginGetResponse異步方法則會拋出一個InvalidOperationException異常,ThreadPool 中沒有足夠的自由線程來完成該操作。但不一定所有異步操作都會有這問題,就像上面的FileStream一樣。

四、不適用線程池的場景

  根據上面試驗得出的結果,再加上本文一開始介紹了ThreadPool適用的場景,現在也說說線程池不適用的場景。如果要調整線程的優先級的話,還是自己開線程吧!線程池內的所有線程都是默認Normal優先級的。如果任務執行的時間比較長的話,建議還是自己開線程,因為有可能阻塞了線程池里面的線程最終導致線程池的線程被耗光。如果任務是要馬上執行的,建議還是使用線程池,因為往線程池提交的任務都需要排隊,線程池建立新線程的速度不多於1秒兩個。

  最后附上老趙三篇博客的連接,各位覺得在下有什么說錯的歡迎批評指正,有什么建議或意見盡管說說。謝謝!

淺談線程池(上):線程池的作用及CLR線程池

淺談線程池(中):獨立線程池的作用及IO線程池

淺談線程池(下):相關試驗及注意事項


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM