最近在做站時發現,線程池的問題很棘手,所以總結了一篇關於線程池的文章,原文地址:http://www.shuonar.com/blog/ac16496b-87ec-4790-a9ea-d69bbffa1a87.html
在C#編程語言中,使用線程池可以並行地處理工作,.NETFramework提供了包含ThreadPool類的System.Threading 空間,這是一個可直接訪問的靜態類,該類對線程池是必不可少的。它是公共“線程池”設計樣式的實現。對於后台運行許多各不相同的任務是有用的。對於單個的后台線程而言有更好的選項。
線程的最大數量。這是完全無須知道的。在.NET中ThreadPool的所有要點是它自己在內部管理線程池中線程。多核機器將比以往的機器有更多的線程。微軟如此陳述“線程池通常有一個線程的最大數量,如果所有的線程都忙,增加的任務被放置在隊列中直到它們能被服務,才能作為可用的線程。”
用法位置
線程池類型能被用於服務器和批處理應用程序中,線程池有更廉價的得到線程的內部邏輯,因為當需要時這些線程已被形成和剛好“連接”,所以線程池風格代碼被用在服務器上。
MSDN表述:“線程池經常用在服務器應用程序中,每一個新進來的需求被分配給一個線程池中的線程,這樣該需求能被異步的執行,沒有阻礙主線程或推遲后繼需求的處理。”
雖然線程池能大大提高服務器的並發性能,但使用它也會存在一定風險。與所有多線程應用程序一樣,用線程池構建的應用程序容易產生各種並發問題,如對共享資源的競爭和死鎖。此外,如果線程池本身的實現不健壯,或者沒有合理地使用線程池,還容易導致與線程池有關的死鎖、系統資源不足和線程泄漏等問題。
1.死鎖
任何多線程應用程序都有死鎖風險。造成死鎖的最簡單的情形是,線程A持有對象X的鎖,並且在等待對象Y的鎖,而線程B持有對象Y的鎖,並且在等待對象X的鎖。線程A與線程B都不釋放自己持有的鎖,並且等待對方的鎖,這就導致兩個線程永遠等待下去,死鎖就這樣產生了。
雖然任何多線程程序都有死鎖的風險,但線程池還會導致另外一種死鎖。在這種情形下,假定線程池中的所有工作線程都在執行各自任務時被阻塞,它們都在等待某個任務A的執行結果。而任務A依然在工作隊列中,由於沒有空閑線程,使得任務A一直不能被執行。這使得線程池中的所有工作線程都永遠阻塞下去,死鎖就這樣產生了。
2.系統資源不足
如果線程池中的線程數目非常多,這些線程會消耗包括內存和其他系統資源在內的大量資源,從而嚴重影響系統性能。
3.並發錯誤
線程池的工作隊列依靠wait()和notify()方法來使工作線程及時取得任務,但這兩個方法都難於使用。如果編碼不正確,可能會丟失通知,導致工作線程一直保持空閑狀態,無視工作隊列中需要處理的任務。因此使用這些方法時,必須格外小心,即便是專家也可能在這方面出錯。最好使用現有的、比較成熟的線程池。例如,直接使用java.util.concurrent包中的線程池類。
4.線程泄漏
使用線程池的一個嚴重風險是線程泄漏。對於工作線程數目固定的線程池,如果工作線程在執行任務時拋出 RuntimeException 或Error,並且這些異常或錯誤沒有被捕獲,那么這個工作線程就會異常終止,使得線程池永久失去了一個工作線程。如果所有的工作線程都異常終止,線程池就最終變為空,沒有任何可用的工作線程來處理任務。
導致線程泄漏的另一種情形是,工作線程在執行一個任務時被阻塞,如等待用戶的輸入數據,但是由於用戶一直不輸入數據(可能是因為用戶走開了),導致這個工作線程一直被阻塞。這樣的工作線程名存實亡,它實際上不執行任何任務了。假如線程池中所有的工作線程都處於這樣的阻塞狀態,那么線程池就無法處理新加入的任務了。
5.任務過載
當工作隊列中有大量排隊等候執行的任務時,這些任務本身可能會消耗太多的系統資源而引起系統資源缺乏。
綜上所述,線程池可能會帶來種種風險,為了盡可能避免它們,使用線程池時需要遵循以下原則。
(1)如果任務A在執行過程中需要同步等待任務B的執行結果,那么任務A不適合加入到線程池的工作隊列中。如果把像任務A一樣的需要等待其他任務執行結果的任務加入到工作隊列中,可能會導致線程池的死鎖。
(2)如果執行某個任務時可能會阻塞,並且是長時間的阻塞,則應該設定超時時間,避免工作線程永久的阻塞下去而導致線程泄漏。在服務器程序中,當線程等待客戶連接,或者等待客戶發送的數據時,都可能會阻塞。可以通過以下方式設定超時時間:
調用ServerSocket的setSoTimeout(int timeout)方法,設定等待客戶連接的超時時間,參見本章3.5.1節(SO_TIMEOUT選項);
對於每個與客戶連接的Socket,調用該Socket的setSoTimeout(inttimeout)方法,設定等待客戶發送數據的超時時間,參見本書第2章的2.5.3節(SO_TIMEOUT選項)。
(3)了解任務的特點,分析任務是執行經常會阻塞的I/O操作,還是執行一直不會阻塞的運算操作。前者時斷時續地占用CPU,而后者對CPU具有更高的利用率。預計完成任務大概需要多長時間?是短時間任務還是長時間任務?
根據任務的特點,對任務進行分類,然后把不同類型的任務分別加入到不同線程池的工作隊列中,這樣可以根據任務的特點,分別調整每個線程池。
(4)調整線程池的大小。線程池的最佳大小主要取決於系統的可用CPU的數目,以及工作隊列中任務的特點。假如在一個具有 N 個CPU的系統上只有一個工作隊列,並且其中全部是運算性質(不會阻塞)的任務,那么當線程池具有 N 或 N+1 個工作線程時,一般會獲得最大的 CPU 利用率。
如果工作隊列中包含會執行I/O操作並常常阻塞的任務,則要讓線程池的大小超過可用CPU的數目,因為並不是所有工作線程都一直在工作。選擇一個典型的任務,然后估計在執行這個任務的過程中,等待時間(WT)與實際占用CPU進行運算的時間(ST)之間的比例WT/ST。對於一個具有N個CPU的系統,需要設置大約N×(1+WT/ST)個線程來保證CPU得到充分利用。
當然,CPU利用率不是調整線程池大小過程中唯一要考慮的事項。隨着線程池中工作線程數目的增長,還會碰到內存或者其他系統資源的限制,如套接字、打開的文件句柄或數據庫連接數目等。要保證多線程消耗的系統資源在系統的承載范圍之內。
(5)避免任務過載。服務器應根據系統的承載能力,限制客戶並發連接的數目。當客戶並發連接的數目超過了限制值,服務器可以拒絕連接請求,並友好地告知客戶:服務器正忙,請稍后再試。