C#線程基礎在前幾篇博文中都介紹了,現在最後來挖掘一下線程池的管理機制,也算為這個線程基礎做個完結。
我們現在都知道了,線程池線程分為工作者線程和I/O線程,他們是怎麼管理的?
對於Microsoft設計的CLR線程池,線程池會隨著CLR的每個版本的發布,都會發生變化,很難去挖掘,這裡的提議是:
最好將線程看成一個黑盒。不要拿單個應用程式去衡量這個黑盒的效能,因為它對任何一個應用程式來說都無法做到完美。
相反,它是一種常規用途的線程調度技術,面向大量應用程式;它對某些應用程式的效果要好於其他應用程式。
目前,它的工作情況非常理想,這裡建議你信任它,因為你很難高出一個比CLR內建的那個更好的線程池。另外,隨著時間的推移,線程池代碼內部,會更改它管理線程的方式,所以大多數應用程式的效能會變得越來越好。
CLR允許開發人員設定線程池建立最大線程數。然後有些開發人員感覺好像有必要對線程池擁有的線程數量進行限制,因為有些人覺得,要合理利用資源,做到自己調配資源,是很有成就感的事(是不是強迫症?)
但實踐證明,線程池永遠都不應該為池中的線程數設定上限,因為可能發生饑餓或死結。
為什麼這麼說?
假如隊列中有1000個工作項目,但這些工作項目全都因為一個事件而阻塞(多麼可怕的事),等到第1001個工作項目發出訊號才能解除阻塞。如果設定最大1000個線程,第1001個線程就不會執行,所以1000個線程會一直阻塞,然後你能想到的,使用者被迫終止應用程式,並丟失他們的所有未儲存的工作。你不能讓線程阻塞!
由於存在饑餓和死結問題,所以CLR團隊一直都在穩步的增加線程池預設能擁有的最大線程數。
目前預設值是最大1000個。這可以看成是不限數量,為什嗎?
一個32位進程最大的2GB的可用地址空間,載入了一組Win32和CLR DLLs,並分配了本地堆和託管堆之後,剩餘約1.5GB的地址空間。由於每個線程都要為使用者模式棧和線程環境塊準備超過1MB的記憶體,所以在一個32位的進程中,最多能有1360個線程。試圖建立更多線程,則會拋出OutMemoryException。
一個64位進程提供了8TB的地址空間,所以理論上可以建立千百萬個線程。但是分配這麼多線程,純屬浪費,尤其是當理想線程數等於機器的CPU數的時候。
ThreadPool類提供了幾個靜態方法,調用它們可以設定和查詢線程池的線程數:GetMaxThreads,SetMaxThreads,GetMinThreads和GetAvailableThreads。這裡建議你,不要調用上述任何方法,限制線程池的線程數,一般只會造成應用程式的效能變得更差,而不會變得更好。
如果你認為自己的應用程式需要幾百個或者幾千個線程,那隻表明,你的應用程式的架構和使用線程的方式已出現嚴重的問題。
現在來看看如何管理工作者線程,之前需要來看看CLR線程池是什麼樣的:
這是工作者線程的資料結構。ThreadPool.QueueUserWorkItem方法和Timer類總是會將工作項目放到全域隊列中。
而背景工作執行緒採用一個先入先出(FIFO)演算法將工作項目從這個隊列取出,並處理它們。(學過資料結構的應該知道FIFO)
由於多個工作者線程可能同時從全域隊列中拿走工作項目,所以所有工作者線程都競爭一個線程同步鎖,以保證兩個或多個線程不會擷取同一個工作項目。同步鎖在某些應用程式總可能對伸縮性和效能造成某種程度的限制。
當一個非工作者線程調度一個Task時,Task會添加到全域隊列。但是,每個工作者線程都有它自己的本地隊列,可以看到,工作者線程是主,對應的本地隊列是附,當一個工作者線程調度一個Task時,Task會添加到調用線程的本地隊列,而不是全域隊列。
現在來看下工作者線程的描述:
工作者線程之所以稱為Workers,它是名副其實的。它就是一“工作狂”,打個比方:
工作狂是什嗎?做完自己的事還不夠,還要去搶別人的事做,別人的事做完了,就去找公用的事做,除非沒有事幹,要不然不會停下。
用這個比方,下面我的介紹就會淺顯很多了。
一個工作者線程準備處理一個工作項目時,它總是先檢查它的本地隊列來尋找一個Task。如果存在Task,工作者線程就從它的本地隊列中移除Task,並對工作項目進行處理。
要注意的是,工作者線程是採用一個“棧”式結構,也就是後入先出(LIFO)演算法,將任務從它的本隊隊列中取出。由於工作者線程是唯一允許訪問自己的本地隊列頭的線程,所以不需要同步鎖,而且在隊列中添加和刪除任務的速度非常快,這個行為的副作用就是,它的執行順序是相反的,後入的先執行。
還有哦,如果一個工作者線程發現本地隊列變空了,那麼它就會嘗試從另一個工作者線程的本地隊列中“偷”一個Task,並擷取一個線程同步鎖,不過這種情況還是很少發生的。
再是,當所有本地隊列都為空白了,工作者線程就使用FIFO演算法,從全域隊列中提取一個工作項目,當然也會取得它的鎖。
現在所有隊列都為空白了,工作者線程就會自己進入睡眠狀態,等待事情的發生。如果睡眠了時間太長,它會自己醒來,並銷毀自身。
線程池會快速建立工作者線程,工作者線程的數量等於ThreadPool的SetMinThreads方法的值(預設是你的電腦CPU數),32位進程最多用32個CPU,64位進程最多可用64個CPU。然後建立工作者線程達到機器CPU數時,線程池會監視工作項目的完成速度,如果工作項目完成的時間太長,線程池就會建立更多的工作者線程,使工作加速完成。如果工作項目的完成速度開始變快了,工作者線程就會被銷毀。
線程池的設計是很人性話的,有沒有體會到?
線程基礎用了這麼久才介紹完,新的起點又來啦。^_^