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數時,線程池會監視工作項的完成速度,如果工作項完成的時間太長,線程池就會創建更多的工作者線程,使工作加速完成。如果工作項的完成速度開始變快了,工作者線程就會被銷毀。
線程池的設計是很人性話的,有沒有體會到?
線程基礎用了這么久才介紹完,新的起點又來啦。^_^