1. 程式人生 > >C#執行緒篇---執行緒池如何管理執行緒(6完結篇)

C#執行緒篇---執行緒池如何管理執行緒(6完結篇)

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,GetMinThreadsGetAvailableThreads。這裡建議你,不要呼叫上述任何方法,限制執行緒池的執行緒數,一般只會造成應用程式的效能變得更差,而不會變得更好

  如果你認為自己的應用程式需要幾百個或者幾千個執行緒,那隻表明,你的應用程式的架構和使用執行緒的方式已出現嚴重的問題。

現在來看看如何管理工作者執行緒,之前需要來看看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數時,執行緒池會監視工作項的完成速度,如果工作項完成的時間太長,執行緒池就會建立更多的工作者執行緒,使工作加速完成。如果工作項的完成速度開始變快了,工作者執行緒就會被銷燬。

  執行緒池的設計是很人性話的,有沒有體會到?

  執行緒基礎用了這麼久才介紹完,新的起點又來啦。^_^