.NET线程池
摘要
深度探索?Microsoft .NET提供的線程池,?揭示什么情況下你需要用線程池以及?.NET框架下的線程池是如何實現(xiàn)的,并告訴你如何去使用線程池。
?
內(nèi)容
介紹
.NET中的線程池
線程池中執(zhí)行的函數(shù)
使用定時器
同步對象的執(zhí)行
異步I/O操作
監(jiān)視線程池
死鎖
有關(guān)安全性
結(jié)束
?
介紹
?????????如果你有在任何編程語言下的多線程編程經(jīng)驗的話,你肯定已經(jīng)非常熟悉一些典型的范例。通常,多線程編程與基于用戶界面的應(yīng)用聯(lián)系在一起,它們需要在不影響終端用戶的情況下,執(zhí)行一些耗時的操作。取出任何一本參考書,打開有關(guān)線程這一章:你能找到一個能在你的用戶界面中并行執(zhí)行數(shù)學(xué)運(yùn)算的多線程示例嗎?
我的目的不是讓你扔掉你的書,不要這樣做!多線程編程技術(shù)使基于用戶界面的應(yīng)用更完美。實際上,?Microsoft .NET框架支持在任何語言編寫的窗口下應(yīng)用多線程編程技術(shù),允許開發(fā)人員設(shè)計非常豐富的界面,提供給終端用戶一個更好的體驗。但是,多線程編程技術(shù)不僅僅是為了用戶界面的應(yīng)用,在沒有任何用戶界面的應(yīng)用中,一樣會出現(xiàn)多個執(zhí)行流的情況。
我們用一個“硬件商店”的客戶/服務(wù)器應(yīng)用系統(tǒng)作為例子。客戶端是收銀機(jī),服務(wù)端是運(yùn)行在倉庫里一臺獨(dú)立的機(jī)器上的應(yīng)用系統(tǒng)。你可以想象一下,服務(wù)器沒有任何的用戶界面,如果不用多線程技術(shù)你將如何去實現(xiàn)?
服務(wù)端通過通道(http, sockets, files?等等)接收來自客戶端的請求并處理它們,然后發(fā)送一個應(yīng)答到客戶端。圖1顯示了它是如何運(yùn)作的。
?
圖1:?單線程的服務(wù)端應(yīng)用系統(tǒng)
為了讓客戶端的請求不會遺漏,服務(wù)端應(yīng)用系統(tǒng)實現(xiàn)了某種隊列來存放這些請求。圖1顯示了三個請求同時到達(dá),但只有其中的一個被服務(wù)端處理。當(dāng)服務(wù)端開始執(zhí)行?"Decrease stock of monkey wrench,"?這個請求時,其它兩個必須在隊列中等待。當(dāng)?shù)谝粋€執(zhí)行完成后,接著是第二個,以此類推。這種方法普遍用于許多現(xiàn)有的系統(tǒng),但是這樣做系統(tǒng)的資源利用率很低。假設(shè)?“decreasing the stock”請求修改磁盤上的一個文件,而這個文件正在被修改中,CPU將不會被使用,即使這個請求正處在待處理階段。這類系統(tǒng)的一個普遍特征就是低CPU利用時間導(dǎo)致出現(xiàn)很長的響應(yīng)時間,甚至是在訪問壓力很大的環(huán)境里也這樣。
?????????另外一個策略就是在當(dāng)前的系統(tǒng)中為每一個請求創(chuàng)建不同的線程。當(dāng)一個新的請求到達(dá)之后,服務(wù)端為進(jìn)入的請求創(chuàng)建一個新線程,執(zhí)行結(jié)束時,再銷毀它。下圖說明了這個過程:
?
?????????圖2:多線程服務(wù)端應(yīng)用系統(tǒng)
就像如圖2所示的那樣。我們有了較高的CPU利用率。即使它已經(jīng)不再像原來的那樣慢了,但創(chuàng)建線和銷毀程也不是最恰當(dāng)?shù)姆椒ā<僭O(shè)線程的執(zhí)行操作不復(fù)雜,由于需要花額外的時間去創(chuàng)建和銷毀線程,所以最終會嚴(yán)重影響系統(tǒng)的響應(yīng)時間。另外一點(diǎn)就是在壓力很大的環(huán)境下,這三個線程會給系統(tǒng)帶來很多的沖擊。多個線程同時執(zhí)行請求處理將導(dǎo)致CPU的利用率達(dá)到100%,而且大多數(shù)時間會浪費(fèi)在上下文切換過程中,甚至?xí)^處理請求的本身。這類系統(tǒng)的典型特征是大量的訪問會導(dǎo)致響應(yīng)時間呈指數(shù)級增長和很高的CUP使用時間。
?????????一個最優(yōu)的實現(xiàn)是綜合前面兩種方案而提出的觀點(diǎn)----線程池(Thread Pool),當(dāng)一個請求達(dá)到時,應(yīng)用系統(tǒng)把置入接收隊列,一組的線程從隊列提取請求并處理之。這個方案如下圖所示:
?
?
圖3:啟用線程池的服務(wù)端應(yīng)用系統(tǒng)
在這個例子中,我們用了一個含有兩個線程的線程池。當(dāng)三個請求到達(dá)時,它們立刻安排到隊列等待被處理,因為兩個線程都是空閑的,所以頭兩個請求開始執(zhí)行。當(dāng)其中任何一個請求處理結(jié)束后,空閑的線程就會去提取第三個請求并處理之。在這種場景中,系統(tǒng)不需要為每個請求創(chuàng)建和銷毀線程。線程之間能互相利用。而且如果線程池的執(zhí)行高效的話,它能增加或刪除線程以獲得最優(yōu)的性能。例如當(dāng)線程池在執(zhí)行兩個請求時,而CPU的利用率才達(dá)到50%,這表明執(zhí)行請求正等待某個事件或者正在做某種I/O操作。線程池可以發(fā)現(xiàn)這種情況,并增加線程的數(shù)量以使系統(tǒng)能在同一時間處理更多的請求。相反的,如果CPU利用率達(dá)到100%,線程池可以減少線程的數(shù)量以獲得更多的CPU時間,而不要浪費(fèi)在上下文切換上面。
?
.NET中的線程池
?????????基于上面的例子,在企業(yè)級應(yīng)用系統(tǒng)中有一個高效執(zhí)行的線程池是至關(guān)重要的。Microsoft在.NET框架的開發(fā)環(huán)境中已經(jīng)實現(xiàn)了這個,該系統(tǒng)的核心提供了一個現(xiàn)成可用的最優(yōu)線程池。
這個線程池不僅對應(yīng)用程序可用,而且還融合到框架中的多數(shù)類中。.NET?建立在同一個池上是一個很重要的功能特性。比如?.NET Remoting?用它來處理來自遠(yuǎn)程對象的請求。
?????????當(dāng)一個托管應(yīng)用程序開始執(zhí)行時,運(yùn)行時環(huán)境(runtime)提供一個線程池,它將在代碼第一次訪問時被創(chuàng)建。這個池與應(yīng)用程序所在運(yùn)行的物理進(jìn)程關(guān)聯(lián)在一起,當(dāng)你用.NET框架下的同一進(jìn)程中運(yùn)行多個應(yīng)用程序的功能特性時(稱之為應(yīng)用程序域),這將是一個很重要的細(xì)節(jié)。在這種情況下,由于它們都使用同樣的線程池,一個壞的應(yīng)用程序會影響進(jìn)程中的其它應(yīng)用程序。
?????????你可以通過System.Threading?名稱空間的Thread Pool?類來使用線程池,如果你查看一下這個類,就會發(fā)現(xiàn)所有的成員都是靜態(tài)的,而且沒有公開的構(gòu)造函數(shù)。這是有理由這樣做的,因為每個進(jìn)程只有一個線程池,并且我們不能創(chuàng)建新的。這個限制的目的是為了把所有的異步編程技術(shù)都集中到同一個池中。所以我們不能擁有一個通過第三方組建創(chuàng)建的無法管理的線程池。
?
線程池中執(zhí)行的函數(shù)
ThreadPool.QueueUserWorkItem?方法運(yùn)行我們在系統(tǒng)線程池上啟動一個函數(shù),它的聲明如下:
public static bool QueueUserWorkItem (WaitCallback callBack, object state) 第一個參數(shù)指明我們將在池中執(zhí)行的函數(shù),它的聲明必須與WaitCallback代理(delegate)互相匹配:public delegate void WaitCallback (object state);State?參數(shù)允許任何類型的信息傳遞到該方法中,它在調(diào)用QueueUserWorkItem時傳入。
?
讓我們結(jié)合這些新概念,看看“硬件商店”的另一個實現(xiàn)。
using System; using System.Threading; namespace ThreadPoolTest { ?? class MainApp ?? { ????? static void Main() ????? { ???????? WaitCallback callBack; ???????? callBack = new WaitCallback(PooledFunc); ???????? ThreadPool.QueueUserWorkItem(callBack, ??????????? "Is there any screw left?"); ???????? ThreadPool.QueueUserWorkItem(callBack, ??????????? "How much is a 40W bulb?"); ???????? ThreadPool.QueueUserWorkItem(callBack, ??????????? "Decrease stock of monkey wrench");?? ?????????Console.ReadLine(); ????? } ? ????? static void PooledFunc(object state) ????? { ???????? Console.WriteLine("Processing request '{0}'", (string)state); ???????? // Simulation of processing time ???????? Thread.Sleep(2000); ???????? Console.WriteLine("Request processed"); ????? } ?? } }為了簡化例子,我們在Main?類中創(chuàng)建一個靜態(tài)方法用于處理請求。由于代理的靈活性,我們可以指定任何實例方法去處理請求,只要這些方法的聲明與代理相同。在這里范例中,通過調(diào)用Thread.Sleep,實現(xiàn)延遲兩秒以模擬處理時間。
你如果編譯和執(zhí)行這個范例,將會看到下面的輸出:
Processing request 'Is there any screw left?' Processing request 'How much is a 40W bulb?' Processing request 'Decrease stock of monkey wrench' Request processed Request processed Request processed?
注意,所有的請求都被不同的線程并行處理了。
我們可以通過在兩個方法中加入如下的代碼,以此看到更多的信息。
?// Main method ?? Console.WriteLine("Main thread. Is pool thread: {0}, Hash: {1}", ??????????? Thread.CurrentThread.IsThreadPoolThread, ????????????Thread.CurrentThread.GetHashCode()); ?? // Pool method ?? Console.WriteLine("Processing request '{0}'." + ??????" Is pool thread: {1}, Hash: {2}", ????? (string)state, Thread.CurrentThread.IsThreadPoolThread, ??????Thread.CurrentThread.GetHashCode()); ?我們增加了一個Thread.CurrentThread.IsThreadPoolThread的調(diào)用。如果目標(biāo)線程屬于線程池,這個屬性將返回True。另外,我們還顯示了用GetHashCode?方法從當(dāng)前線程返回的結(jié)果。它是唯一標(biāo)識當(dāng)前執(zhí)行線程的值。現(xiàn)在看一看這個輸出結(jié)果:
Main thread. Is pool thread: False, Hash: 2 Processing request 'Is there any screw left?'. Is pool thread: True, Hash: 4 Processing request 'How much is a 40W bulb?'. Is pool thread: True, Hash: 8 Processing request 'Decrease stock of monkey wrench '. Is pool thread: True, Hash: 9 Request processed Request processed Request processed ?你可以看到所有的請求都被系統(tǒng)線程池中的不同線程執(zhí)行。再次運(yùn)行這個例子,注意系統(tǒng)CPU的利用率,如果你沒有任何其它應(yīng)用程序在后臺運(yùn)行的話,它幾乎是0%。因為系統(tǒng)唯一正在做的是每執(zhí)行2秒后就掛起的處理。
?????????我們來修改一下這個應(yīng)用,這次我們不掛起處理請求的線程,相反我們會一直讓系統(tǒng)忙,為了做到這點(diǎn),我們用Environment.TickCount.?構(gòu)建一個每隔兩秒就對請求執(zhí)行一次的循環(huán)。
int ticks = Environment.TickCount; while(Environment.TickCount - ticks < 2000);現(xiàn)在打開任務(wù)管理器,看一看CPU的使用率,你將看到應(yīng)用程序占有了CPU的100%的使用率。再看一下我們程序的輸出結(jié)果:
Processing request 'Is there any screw left?'. Is pool thread: True, Hash: 7 Processing request 'How much is a 40W bulb?'. Is pool thread: True, Hash: 8 Request processed Processing request 'Decrease stock of monkey wrench '. Is pool thread: True, Hash: 7 Request processed Request processed ?注意第三個請求是在第一個請求處理結(jié)束之后執(zhí)行的,而且線程的號碼仍然用原來的7,這個原因是線程池檢測到CPU的使用率已經(jīng)達(dá)到100%,一直等待某個線程空閑。它并不會重新創(chuàng)建一個新的線程,這樣就會減少線程間的上下文切換開銷,以使總體性能更佳。
?
使用定時器
假如你曾經(jīng)開發(fā)過Microsoft Win32的應(yīng)用程序,你知道SetTimer函數(shù)是API之一,通過這個函數(shù)可以指定的一個窗口接收到來自系統(tǒng)時間周期的WM_TIMER消息。用這個方法遇到的第一個問題是你需要一個窗口去接收消息,所以你不能用在控制臺應(yīng)用程序中。另外,基于消息的實現(xiàn)并不是非常精確,假如你的應(yīng)用程序正在處理其它消息,情況有可能更糟糕。
相對基于Win32的定時器來說,?.NET?中一個很重要的改進(jìn)就是創(chuàng)建不同的線程,該線程阻塞指定的時間,然后通知一個回調(diào)函數(shù)。這里的定時器不需要Microsoft的消息系統(tǒng),所以這樣就更精確,而且還能用于控制臺應(yīng)用程序中。以下代碼顯示了這個技術(shù)的一種實現(xiàn):
class MainApp { ?? static void Main() ?? { ????? MyTimer myTimer = new MyTimer(2000); ????? Console.ReadLine(); ?? } } class MyTimer { ?? int m_period; ?? public MyTimer(int period) ?? { ????? Thread thread; ????? m_period = period; ????? thread = new Thread(new ThreadStart(TimerThread)); ????? thread.Start(); ?? } ?? void TimerThread() ?? { ????? Thread.Sleep(m_period); ????? OnTimer(); ?? } ?? void OnTimer() ?? { ????? Console.WriteLine("OnTimer"); ?? } }這個代碼一般用于Wn32應(yīng)用中。每個定時器創(chuàng)建獨(dú)立的線程,并且等待指定的時間,然后呼叫回調(diào)函數(shù)。猶如你看到的那樣,這個實現(xiàn)的成本會非常高。如果你的應(yīng)用程序使用了多個定時器,相對的線程數(shù)量也會隨著使用定時器的數(shù)量而增長。
現(xiàn)在我們有.NET?提供的線程池,我們可以從池中改變請求的等待函數(shù),這樣就十分有效,而且會提升系統(tǒng)的性能。我們會遇到兩個問題:
n??????????假如線程池已滿(所有的線程都在運(yùn)行中),那么這個請求排到隊列中等待,而且定時器不在精確。
n??????????假如創(chuàng)建了多個定時器,線程池會因為等待它們時間片失效而非常忙。
為了避免這些問題,.NET框架的線程池提供了獨(dú)立于時間的請求。用了這個函數(shù),我們可以不用任何線程就可以擁有成千上萬個定時器,一旦時間片失效,這時,線程池將會處理這些請求。
這些特色出現(xiàn)在兩個不同的類中:
?????????System.Threading.Timer
???????????????????定時器的簡單版本,它運(yùn)行開發(fā)人員向線程池中的定期執(zhí)行的程序指定一個代理(delegate).
System.Timers.Timer
System.Threading.Timer的組件版本,允許開發(fā)人員把它拖放到一個窗口表單(form)中,可以把一個事件作為執(zhí)行的函數(shù)。
這非常有助于理解上述兩個類與另外一個稱為System.Windows.Forms.Timer.的類。這個類只是封裝了Win32中消息機(jī)制的計數(shù)器,如果你不準(zhǔn)備開發(fā)多線程應(yīng)用,那么就可以用這個類。
在下面的例子中,我們將用System.Threading.Timer?類,定時器的最簡單實現(xiàn),我們只需要如下定義的構(gòu)造方法
public Timer(TimerCallback callback, ?? object state, ?? int dueTime, ?? int period);對于第一個參數(shù)(callback),我們可以指定定時執(zhí)行的函數(shù);第二個參數(shù)是傳遞給函數(shù)的通用對象;第三個參數(shù)是計時器開始執(zhí)行前的延時;最后一個參數(shù)period,是兩個執(zhí)行之間的毫秒數(shù)。
下面的例子創(chuàng)建了兩個定時器,timer1和timer2:
class MainApp { ?? static void Main() ?? { ????? Timer timer1 = new Timer(new TimerCallback(OnTimer), 1, 0, 2000); ????? Timer timer2 = new Timer(new TimerCallback(OnTimer), 2, 0, 3000); ????? Console.ReadLine(); ?? } ?? static void OnTimer(object obj) ?? { ????? Console.WriteLine("Timer: {0} Thread: {1} Is pool thread: {2}", ?????????(int)obj, ???????? Thread.CurrentThread.GetHashCode(), ??? ?????Thread.CurrentThread.IsThreadPoolThread); ?? } }輸出:
Timer: 1 Thread: 2 Is pool thread: True Timer: 2 Thread: 2 Is pool thread: True Timer: 1 Thread: 2 Is pool thread: True Timer: 2 Thread: 2 Is pool thread: True Timer: 1 Thread: 2 Is pool thread: True Timer: 1 Thread: 2 Is pool thread: True Timer: 2 Thread: 2 Is pool thread: True猶如你看到的那樣,兩個定時器中的所有函數(shù)調(diào)用都在同一個線程中執(zhí)行(ID = 2),應(yīng)用程序使用的資源最小化了。
?
同步對象的執(zhí)行
相對于定時器,.NET線程池允許在執(zhí)行函數(shù)上同步對象,為了在多線程環(huán)境中的各線程之間共享資源,我們需要用.NET同步對象。
如果我們沒有線程,或者線程必須阻塞直到事件收到信號,就像我前面提到一樣,這會增加應(yīng)用程序中總的線程數(shù)量,結(jié)果導(dǎo)致系統(tǒng)需要更多的資源和CPU時間。
線程池允許我們把請求進(jìn)行排隊,直到某個特殊的同步對象收到信號后執(zhí)行。如果這個信號沒有收到,請求函數(shù)將不需要任何線程,所以可以保證系統(tǒng)性能最優(yōu)化。ThreadPool類提供了下面的方法:
public static RegisteredWaitHandle RegisterWaitForSingleObject( ?? WaitHandle waitObject, ?? WaitOrTimerCallback callBack, ?? object state, ?? int millisecondsTimeOutInterval, ?? bool executeOnlyOnce);?
第一個參數(shù),waitObject?可以是任何繼承于WaitHandle的對象:
???????? Mutex
???? ManualResetEvent
???? AutoResetEvent
就像你看到的那樣,只有系統(tǒng)的同步對象才能用在這里,就是繼承自WaitHandle的對象。你不能用其它任何的同步機(jī)制,比如moniter?或者?read-write?鎖。剩余的參數(shù)允許我們指明當(dāng)一個對象收到信號后執(zhí)行的函數(shù)(callBack);一個傳遞給函數(shù)的狀態(tài)(state);?線程池等待對象的最大時間?(millisecondsTimeOutInterval)?和一個標(biāo)識表明對象收到信號時函數(shù)只能執(zhí)行一次,?(executeOnlyOnce).?下面的代理聲明目的是用在函數(shù)的回調(diào):
delegate void WaitOrTimerCallback( ?? object state, ?? bool timedOut); ? 如果參數(shù) timeout 設(shè)置的最大時間已經(jīng)失效,但是沒有同步對象收到信號的花,這個函數(shù)就會被調(diào)用。 下面的例子用了一個手工事件和一個互斥量來通知線程池中的執(zhí)行函數(shù): class MainApp { ?? static void Main(string[] args) ?? { ????? ManualResetEvent evt = new ManualResetEvent(false); ????? Mutex mtx = new Mutex(true); ????? ThreadPool.RegisterWaitForSingleObject(evt, ???????? new WaitOrTimerCallback(PoolFunc), ???????? null, Timeout.Infinite, true); ????? ThreadPool.RegisterWaitForSingleObject(mtx, ???????? new WaitOrTimerCallback(PoolFunc), ???????? null, Timeout.Infinite, true); ????? for(int i=1;i<=5;i++) ????? { ???????? Console.Write("{0}...", i); ???????? Thread.Sleep(1000); ?? ???} ????? Console.WriteLine(); ????? evt.Set(); ????? mtx.ReleaseMutex(); ????? Console.ReadLine(); ?? } ?? static void PoolFunc(object obj, bool TimedOut) ?? { ????? Console.WriteLine("Synchronization object signaled, Thread: {0} Is pool: {1}", ?????????Thread.CurrentThread.GetHashCode(), ???????? Thread.CurrentThread.IsThreadPoolThread); ?? } }結(jié)束顯示兩個函數(shù)都在線程池的同一線程中執(zhí)行:
1...2...3...4...5... Synchronization object signaled, Thread: 6 Is pool: True Synchronization object signaled, Thread: 6 Is pool: True?
異步I/O操作
線程池常見的應(yīng)用場景就是I/O操作。多數(shù)應(yīng)用系統(tǒng)需要讀磁盤,數(shù)據(jù)發(fā)送到Sockets,因特網(wǎng)連接等等。所有的這些操作都有一些特征,直到他們執(zhí)行操作時,才需要CPU時間。.NET?框架為所有這些可能執(zhí)行的異步操作提供了I/O類。當(dāng)這些操作執(zhí)行完后,線程池中特定的函數(shù)會執(zhí)行。尤其是在服務(wù)器應(yīng)用程序中執(zhí)行多線程異步操作,性能會更好。
在第一個例子中,我們將把一個文件異步寫到硬盤中。看一看FileStream?的構(gòu)造方法是如何使用的:
public FileStream( ?? string path, ?? FileMode mode, ?? FleAccess access, ?? FleShare share, ?? int bufferSize, ?? bool useAsync);最后一個參數(shù)非常有趣,我們應(yīng)該對異步執(zhí)行文件的操作設(shè)置useAsync為True。如果我們沒有這樣做,即使我們用了異步函數(shù),它們的操作仍然會被主叫線程阻塞。
下面的例子說明了用一旦FileStream BeginWrite方法寫文件操作結(jié)束,線程池中的一個回調(diào)函數(shù)將會被執(zhí)行。注意我們可以在任何時候訪問IAsyncResult接口,它可以用來了解當(dāng)前操作的狀態(tài)。我們可以用CompletedSynchronously?屬性指示一個異步操作是否完成,而當(dāng)一個操作結(jié)束時,IsCompleted?屬性會設(shè)上一個值。IAsyncResult?提供了很多有趣的屬性,比如:AsyncWaitHandle?,一旦操作完成,一個異步對象將會被通知。
class MainApp { ?? static void Main() ?? { ????? const string fileName = "temp.dat"; ????? FileStream fs; ????? byte[] data = new Byte[10000]; ????? IAsyncResult ar; ? ????? fs = new FileStream(fileName, ?????????FileMode.Create, ?????????FileAccess.Write, ?????????FileShare.None, ?????????1, ?????????true); ????? ar = fs.BeginWrite(data, 0, 10000, ???????? new AsyncCallback(UserCallback), null); ????? Console.WriteLine("Main thread:{0}", ???????? Thread.CurrentThread.GetHashCode()); ????? Console.WriteLine("Synchronous operation: {0}", ???????? ar.CompletedSynchronously); ????? Console.ReadLine(); ?? } ?? static void UserCallback(IAsyncResult ar) ?? { ????? Console.Write("Operation finished: {0} on thread ID:{1}, is pool: {2}", ?????????ar.IsCompleted, ?????????Thread.CurrentThread.GetHashCode(), ?????????Thread.CurrentThread.IsThreadPoolThread); ?? } }輸出的結(jié)果顯示了操作是異步執(zhí)行的,一旦操作結(jié)束后,用戶的函數(shù)就在線程池中執(zhí)行。
Main thread:9 Synchronous operation: False Operation finished: True on thread ID:10, is pool: True在應(yīng)用Sockets的場景中,由于I/O操作通常比磁盤操作慢,這時用線程池就顯得尤為重要。過程跟前面提到的差不多,Socket?類提供了多個方法用于執(zhí)行異步操作:
???????? BeginRecieve
???????? BeginSend
???????? BeginConnect
???????? BeginAccept
假如你的服務(wù)器應(yīng)用使用了Socket來與客戶端通訊,一定會用到這些方法。這種方法取代了對每個客戶端連接都啟用一個線程的做法,所有的操作都在線程池中異步執(zhí)行。
?????????下面的例子用另外一個支持異步操作的類,HttpWebRequest。用這個類,我們可以建立一個到Web服務(wù)器的連接。這個方法叫BeginGetResponse,?但在這個例子中有一個很重要的區(qū)別。在上面最后一個示例中,我們沒有用到從操作中返回的結(jié)果。但是,我們現(xiàn)在需要當(dāng)一個操作結(jié)束時從Web服務(wù)器返回的響應(yīng),為了接收到這個信息,.NET中所有提供異步操作的類都提供了成對的方法。在HttpWebRequest這個類中,這個成對的方法就是:BeginGetResponse和EndGetResponse。用了End版本,我們可以接收操作的結(jié)果。在我們的示例中,EndGetResponse?會從Web服務(wù)器接收響應(yīng)。
雖然可以在任何時間調(diào)用EndGetResponse?方法,但在我們的例子中是在回調(diào)函數(shù)中做的。僅僅是因為我們想知道已經(jīng)做了異步請求。如果我們在之前調(diào)用EndGetResponse?,這個調(diào)用將一直阻塞到操作完成。
?????????在下面的例子中,我們發(fā)送一個請求到Microsoft Web,然后顯示了接收到響應(yīng)的大小。
class MainApp { ?? static void Main() ?? { ????? HttpWebRequest request; ????? IAsyncResult ar; ? ????? request = (HttpWebRequest)WebRequest.CreateDefault( ???????? new Uri("http://www.microsoft.com")); ????? ar = request.BeginGetResponse(new AsyncCallback(PoolFunc), request); ????? Console.WriteLine("Synchronous: {0}", ar.CompletedSynchronously); ????? Console.ReadLine(); ?? } ?? static void PoolFunc(IAsyncResult ar) ?? { ????? HttpWebRequest request; ????? HttpWebResponse response; ? ????? Console.WriteLine("Response received on pool: {0}", ???????? Thread.CurrentThread.IsThreadPoolThread); ????? request = (HttpWebRequest)ar.AsyncState; ????? response = (HttpWebResponse)request.EndGetResponse(ar); ????? Console.WriteLine("?Response size: {0}", ???????? response.ContentLength); ?? } }下面剛開始結(jié)果信息表明,異步操作正在執(zhí)行:
Synchronous: False過了一會兒,響應(yīng)接收到了。下面的結(jié)果顯示:
Response received on pool: True ?? Response size: 27331就像你看到的那樣,一旦收到響應(yīng),線程池的異步函數(shù)就會執(zhí)行。
?
監(jiān)視線程池
ThreadPool 類提供了兩個方法用來查詢線程池的狀態(tài)。第一個是我們可以從線程池獲取當(dāng)前可用的線程數(shù)量: public static void GetAvailableThreads( ?? out int workerThreads, ?? out int completionPortThreads);從方法中你可以看到兩種不同的線程:
?????????WorkerThreads
???????工作線程是標(biāo)準(zhǔn)系統(tǒng)池的一部分。它們是被.NET框架托管的標(biāo)準(zhǔn)線程,多數(shù)函數(shù)是在這里執(zhí)行的。顯式的用戶請求(QueueUserWorkItem方法),基于異步對象的方法(RegisterWaitForSingleObject)和定時器(Timer類)
?
CompletionPortThreads
這種線程常常用來I/O操作,Windows NT, Windows 2000?和?Windows XP提供了一個步執(zhí)行的對象,叫做IOCompletionPort。把API和異步對象關(guān)聯(lián)起來,用少量的資源和有效的方法,我們就可以調(diào)用系統(tǒng)線程池的異步I/O操作。但是在Windows 95, Windows 98,?和?Windows Me有一些局限。比如:?在某些設(shè)備上,沒有提供IOCompletionPorts?功能和一些異步操作,如磁盤和郵件槽。在這里你可以看到.NET框架的最大特色:一次編譯,可以在多個系統(tǒng)下運(yùn)行。根據(jù)不同的目標(biāo)平臺,.NET?框架會決定是否使用IOCompletionPorts API,用最少的資源達(dá)到最好的性能。
這節(jié)包含一個使用Socket?類的例子。在這個示例中,我們將異步建立一個連接到本地的Web服務(wù)器,然后發(fā)送一個Get請求。通過這個例子,我們可以很容易地鑒別這兩種不同的線程。
using System; using System.Threading; using System.Net; using System.Net.Sockets; using System.Text; ? namespace ThreadPoolTest { ?? class MainApp ?? { ????? static void Main() ????? { ???????? Socket s; ???????? IPHostEntry hostEntry; ???????? IPAddress ipAddress; ???????? IPEndPoint ipEndPoint; ???????? ?????????hostEntry = Dns.Resolve(Dns.GetHostName()); ???????? ipAddress = hostEntry.AddressList[0]; ???????? ipEndPoint = new IPEndPoint(ipAddress, 80); ???????? s = new Socket(ipAddress.AddressFamily, ??????????? SocketType.Stream, ProtocolType.Tcp); ???????? s.BeginConnect(ipEndPoint, new AsyncCallback(ConnectCallback),s); ?? ?????? ?????????Console.ReadLine(); ????? } ????? static void ConnectCallback(IAsyncResult ar) ????? { ???????? byte[] data; ???????? Socket s = (Socket)ar.AsyncState; ???????? data = Encoding.ASCII.GetBytes("GET /"n"); ? ???????? Console.WriteLine("Connected to localhost:80"); ???????? ShowAvailableThreads(); ???????? s.BeginSend(data, 0,data.Length,SocketFlags.None, ??????????? new AsyncCallback(SendCallback), null); ????? } ????? static void SendCallback(IAsyncResult ar) ????? { ???????? Console.WriteLine("Request sent to localhost:80"); ???????? ShowAvailableThreads(); ????? } ????? static void ShowAvailableThreads() ????? { ???????? int workerThreads, completionPortThreads; ? ???????? ThreadPool.GetAvailableThreads(out workerThreads, ??????????? out completionPortThreads); ???????? Console.WriteLine("WorkerThreads: {0}," + ????????????" CompletionPortThreads: {1}", ??????????? workerThreads, completionPortThreads); ????? } ?? } }?
如果你在Microsoft Windows NT, Windows 2000, or Windows XP?下運(yùn)行這個程序,你將會看到如下結(jié)果:
Connected to localhost:80 WorkerThreads: 24, CompletionPortThreads: 25 Request sent to localhost:80 WorkerThreads: 25, CompletionPortThreads: 24?
如你所看到地那樣,連接用了工作線程,而發(fā)送數(shù)據(jù)用了一個完成端口(CompletionPort),接著看下面的順序:
1.???我們得到一個本地IP地址,然后異步連接到那里。
2.???Socket在工作線程上執(zhí)行異步連接操作,因為在Socket上,不能用Windows?的IOCompletionPorts來建立連接。
3.???一旦連接建立了,Socket類調(diào)用指明的函數(shù)ConnectCallback,這個回調(diào)函數(shù)顯示了線程池中可用的線程數(shù)量。我們可以看到這些是在工作線程中執(zhí)行的。
4.???在用ASCII碼對Get請求進(jìn)行編碼后,我們用BeginSend方法從同樣的函數(shù)ConnectCallback?中發(fā)送一個異步請求。
5.???Socket上的發(fā)送和接收操作可以通過IOCompletionPort?來執(zhí)行異步操作,所以當(dāng)請求做完后,回調(diào)函數(shù)就會在一個CompletionPort類型的線程中執(zhí)行。因為函數(shù)本身顯示了可用的線程數(shù)量,所以我們可以通過這個來查看,對應(yīng)的完成端口數(shù)已經(jīng)減少了多少。
如果我們在Windows 95, Windows 98,?或者?Windows Me平臺上運(yùn)行相同的代碼,會出現(xiàn)相同的連接結(jié)果,請求將被發(fā)送到工作線程,而非完成端口。你應(yīng)該知道的很重要的一點(diǎn)就是,Socket類總是會利用最優(yōu)的可用機(jī)制,所以你在開發(fā)應(yīng)用時,可以不用考慮目標(biāo)平臺是什么。
???????你已經(jīng)看到在上面的例子中每種類型的線程可用的最大數(shù)是25。我們可以用GetMaxThreads返回這個值:
public static void GetMaxThreads( ?? out int workerThreads, ?? out int completionPortThreads);一旦到了最大的數(shù)量,就不會創(chuàng)建新線程,所有的請求都將被排隊。假如你看過ThreadPool類的所有方法,你將發(fā)現(xiàn)沒有一個允許我們更改最大數(shù)的方法。就像我們前面提到的那樣,線程池是每個處理過程的唯一共享資源。這就是為什么不可能讓應(yīng)用程序域去更改這個配置的原因。想象一下出現(xiàn)這種情況的后果,如果有第三方組件把線程池中線程的最大數(shù)改為1,整個應(yīng)用都會停止工作,甚至在進(jìn)程中其它的應(yīng)用程序域都將受到影響。同樣的原因,公共語言運(yùn)行時的宿主也有可能去更改這個配置。比如:ASP.NET允許系統(tǒng)管理員更改這個數(shù)字。
?
死鎖
在你的應(yīng)用程序使用線程池之前,還有一個東西你應(yīng)該知道:死鎖。在線程池中執(zhí)行一個實現(xiàn)不好的異步對象可能導(dǎo)致你的整個應(yīng)用系統(tǒng)中止運(yùn)行。
???????設(shè)想你的代碼中有個方法,它需要通過Socket連接到一個Web服務(wù)器上。一個可能的實現(xiàn)就是用Socket?類中的BeginConnect方法異步打開一個連接,然后用EndConnect方法等待連接的建立。代碼如下:
??????? class ConnectionSocket { ?? public void Connect() ?? { ????? IPHostEntry ipHostEntry = Dns.Resolve(Dns.GetHostName()); ????? IPEndPoint ipEndPoint = new IPEndPoint(ipHostEntry.AddressList[0], ???????? 80); ????? Socket s = new Socket(ipEndPoint.AddressFamily, SocketType.Stream, ???????? ProtocolType.Tcp); ????? IAsyncResult ar = s.BeginConnect(ipEndPoint, null, null); ????? s.EndConnect(ar); ?? } }多快,多好。調(diào)用BeginConnect使異步操作在線程池中執(zhí)行,而EndConnect一直阻塞到連接被建立。
???????如果線程池中的一個執(zhí)行函數(shù)中用了這個類的方法,將會發(fā)生什么事情呢?設(shè)想線程池的大小只有兩個線程,然后用我們的連接類創(chuàng)建了兩個異步對象。當(dāng)這兩個函數(shù)同時在池中執(zhí)行時,線程池已經(jīng)沒有用于其它請求的空間了,除非直到某個函數(shù)結(jié)束。問題是這些函數(shù)調(diào)用了我們類中的Connect方法,這個方法在線程池中又發(fā)起了一個異步操作。但線程池一直是滿的,所以請求就一直等待任何空閑線程的出現(xiàn)。不幸的是,這將永遠(yuǎn)不會發(fā)生,因為使用線程池的函數(shù)正等待隊列函數(shù)的結(jié)束。結(jié)論就是:我們的應(yīng)用系統(tǒng)已經(jīng)阻塞了。
???????我們以此推斷25個線程的線程池的行為。假如25個函數(shù)都等待異步對象操作的結(jié)束。結(jié)果將是一樣的,死鎖一樣會出現(xiàn)。
???????在下面的代碼片斷中,我們使用了這個類來說明問題:
class MainApp { ?? static void Main() ?? { ????? for(int i=0;i<30;i++) ????? { ???????? ThreadPool.QueueUserWorkItem(new WaitCallback(PoolFunc)); ????? } ????? Console.ReadLine(); ?? } ? ?? static void PoolFunc(object state) ??{ ????? int workerThreads,completionPortThreads; ????? ThreadPool.GetAvailableThreads(out workerThreads, ???????? out completionPortThreads); ????? Console.WriteLine("WorkerThreads: {0}, CompletionPortThreads: {1}", ?????????workerThreads, completionPortThreads); ? ????? Thread.Sleep(15000); ????? ConnectionSocket connection = new ConnectionSocket(); ????? connection.Connect(); ?? } }如果你運(yùn)行這個例子,你將看到池中的線程是如何把線程的可用數(shù)量減少到零的,接著應(yīng)用中止,死鎖出現(xiàn)了。
???????如果你想在你的應(yīng)用中避免出現(xiàn)死鎖,永遠(yuǎn)不要阻塞正在等待線程池中的其它函數(shù)的線程。這看起來很容易,但記住這個規(guī)則意味著有兩條:
n??????????不要創(chuàng)建這樣的類,它的同步方法在等待異步函數(shù)。因為這種類可能被線程池中的線程調(diào)用。
n??????????不要在任何異步函數(shù)中使用這樣的類,如果它正等待著這個異步函數(shù)。
如果你想檢測到應(yīng)用中的死鎖情況,那么就當(dāng)你的系統(tǒng)掛起時,檢查線程池中的線程可用數(shù)。線程的可用數(shù)量已經(jīng)沒有并且CPU的使用率為0?,這是很明顯的死鎖癥狀。你應(yīng)該檢查你的代碼,以確定哪個在線程中執(zhí)行的函數(shù)正在等待異步操作,然后刪除它。
??????
有關(guān)安全性
?????????如果你再看看ThreadPool類,你會看到有兩個方法我們沒有用到,UnsafeQueueUserWorkItem和UnsafeRegisterWaitForSingleObject。?為了完全理解這些方法,首先,我們必須回憶?.NET框架中安全策略是怎么運(yùn)作的。
???????? ?Windows安全機(jī)制是關(guān)注資源。操作系統(tǒng)本身允許對文件,用戶,注冊表鍵值和任何其它的系統(tǒng)資源設(shè)定權(quán)限。這種方法對應(yīng)用系統(tǒng)的用戶認(rèn)證非常有效,但當(dāng)出現(xiàn)用戶對他使用的系統(tǒng)產(chǎn)生不信任的情況時,這就會有些局限性。例如這些程序是從Internet下載的。在這種情況下,一旦用戶安裝了這個程序,它就可以執(zhí)行用戶權(quán)限范圍內(nèi)的任何操作。舉個例子,假如用戶可以刪除他公司內(nèi)的任何共享文件,任何從Internet下載的程序也都可以這樣做。
???????? .NET?提供了應(yīng)用到程序的安全性策略,而不是用戶。這就是說,在用戶權(quán)限的范圍內(nèi),我們可以限制任何執(zhí)行單元(程序集)使用的資源。通過MMC,我們可以根據(jù)條件定義一組程序集,然后為每組設(shè)置不同的策略,一個典型的例子就是限制從Internet下載的程序訪問磁盤的權(quán)限。
?????????為了讓這個功能運(yùn)轉(zhuǎn)起來,.NET?框架必須維護(hù)一個不同程序集之間的調(diào)用棧。假設(shè)一個應(yīng)用沒有權(quán)限訪問磁盤,但是它調(diào)用了一個對整個系統(tǒng)都可以訪問的類庫,當(dāng)?shù)诙€程序集執(zhí)行一個磁盤的操作時,設(shè)置到這個程序集的權(quán)限允許這樣做,但是權(quán)限不會被應(yīng)用到主叫程序集,.NET不僅要檢查當(dāng)前程序集的權(quán)限,而且會檢查整個調(diào)用棧的權(quán)限。這個棧已經(jīng)被高度優(yōu)化了,但是它們給兩個不同程序集之間的調(diào)用增加了額外的負(fù)擔(dān)。
?????????UnsafeQueueUserWorkItem?,?UnsafeRegisterWaitForSingleObject與?QueueUserWorkItem?,RegisterWaitForSingleObject兩個方法類似。由于是非安全版本不會維護(hù)它們執(zhí)行函數(shù)之間的調(diào)用棧,所以非安全版本運(yùn)行的更快些。但是回調(diào)函數(shù)將只在當(dāng)前程序集的安全策略下執(zhí)行,它就不能應(yīng)用權(quán)限到整個調(diào)用棧中的程序集。
?????????我的建議是僅在性能非常重要的、安全已經(jīng)控制好的極端情況下才用非安全版本。例如,你構(gòu)建的應(yīng)用程序不會被其它的程序集調(diào)用,或者僅被很明確清楚的程序集使用,那么你可以用非安全版本。如果你開發(fā)的類庫會被第三方應(yīng)用程序中使用,那么你就不應(yīng)該用這些方法,因為它們可能用你的庫獲取訪問系統(tǒng)資源的權(quán)限。
?????????在下面例子中,你可以看到用UnsafeQueueUserWorkItem方法的風(fēng)險。我們將構(gòu)建兩個單獨(dú)的程序集,在第一個程序集中我們將在線程池中創(chuàng)建一個文件,然后我們將導(dǎo)出一個類以使這個操作可以被其它的程序集執(zhí)行。
using System; using System.Threading; using System.IO; namespace ThreadSecurityTest { ?? public class PoolCheck ?? { ????? public void CheckIt() ????? { ???????? ThreadPool.QueueUserWorkItem(new WaitCallback(UserItem), null); ????? } ????? private void UserItem(object obj) ????? { ???????? FileStream fs = new FileStream("test.dat", FileMode.Create); ???????? fs.Close(); ???????? Console.WriteLine("File created"); ????? } ?? } }第二個程序集引用了第一個,并且用了CheckIt?方法去創(chuàng)建一個文件:
using System; namespace ThreadSecurityTest { ?? class MainApp ?? { ????? static void Main() ????? { ???????? PoolCheck pc = new PoolCheck(); ???????? pc.CheckIt(); ???????? Console.ReadLine(); ????? } ?? } }編譯這兩個程序集,然后運(yùn)行main應(yīng)用。默認(rèn)情況下,你的應(yīng)用被配置為允許執(zhí)行磁盤操作,所以系統(tǒng)成功生成文件。
???? File created現(xiàn)在,打開.NET框架的配置。為了簡化這個例子,我們僅創(chuàng)建一個代碼組關(guān)聯(lián)到main應(yīng)用。接著展開?運(yùn)行庫安全策略/?計算機(jī)/?代碼組/ All_Code /,增加一個叫ThreadSecurityTest的組。在向?qū)е?#xff0c;選擇Hash?條件并導(dǎo)入Hash到我們的應(yīng)用中,設(shè)置為Internet級別,并選擇“該策略級別將只具有與此代碼組關(guān)聯(lián)的權(quán)限集中的權(quán)限”選項。
運(yùn)行應(yīng)用程序,看看會發(fā)生什么情況:
Unhandled Exception: System.Security.SecurityException: Request for the ???permission of type System.Security.Permissions.FileIOPermission, ??????mscorlib, Version=1.0.3300.0, Culture=neutral, ?????????PublicKeyToken=b77a5c561934e089 failed.我們的策略開始工作,系統(tǒng)已經(jīng)不能創(chuàng)建文件了。這是因為.NET框架為我們維護(hù)了一個調(diào)用棧才使它成為了可能,雖然創(chuàng)建文件的庫有權(quán)限去訪問系統(tǒng)。
現(xiàn)在把庫中的QueueUserWorkItem替換為UnsafeQueueUserWorkItem,再次編譯程序集,然后運(yùn)行Main程序。現(xiàn)在的結(jié)果是:
File created即使我們的系統(tǒng)沒有足夠的權(quán)限去訪問磁盤,但我們已經(jīng)創(chuàng)建了一個向整個系統(tǒng)公開它的功能的庫,卻沒有維護(hù)它的調(diào)用棧。記住一個金牌規(guī)則:?僅在你的代碼不允許讓其它的應(yīng)用系統(tǒng)調(diào)用,或者當(dāng)你想要嚴(yán)格限制訪問很明確清楚的程序集,才使用非安全的函數(shù)。
?
結(jié)束
?????????在這篇文章中,我們知道了為什么在我們的服務(wù)器應(yīng)用中需要使用線程池來優(yōu)化資源和CPU的利用。我們學(xué)習(xí)了一個線程池是如何實現(xiàn)的,需要考慮多個因素如:CPU使用的百分比,隊列請求或者系統(tǒng)的處理器數(shù)量。
???????? .NET提供了豐富的線程池的功能以讓我們的應(yīng)用程序使用,?并且與.NET框架的類緊密地集成在一起。這個線程池是高度優(yōu)化了的,它只需要最少的CPU時間和資源,而且總能適應(yīng)目標(biāo)平臺。
?????????因為與框架集成在一起,所以框架中的大部分類都提供了使用線程池的內(nèi)在功能,給開發(fā)人員提供了集中管理和監(jiān)視應(yīng)用中的線程池的功能。鼓勵第三方組件使用線程池,這樣它們的客戶就可以享受.NET所提供的全部功能。允許執(zhí)行用戶函數(shù),定時器,I/O操作和同步對象。
?????????假如你在開發(fā)服務(wù)器應(yīng)用系統(tǒng),只要有可能就在你的請求處理系統(tǒng)中使用線程池。或者你開發(fā)了一個讓服務(wù)器程序使用的庫,那么盡可能提供系統(tǒng)線程池的異步對象處理。
轉(zhuǎn)載于:https://www.cnblogs.com/gjhjoy/p/3547996.html
總結(jié)
- 上一篇: 变速车多少钱啊?
- 下一篇: WPF CanExecuteChange