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