如果你有在任何編程語言下的多線程編程經(jīng)驗(yàn)的話,你肯定已經(jīng)非常熟悉一些典型的范例。通常,多線程編程與基于用戶界面的應(yīng)用聯(lián)系在一起,它們需要在不影響終端用戶的情況下,執(zhí)行一些耗時(shí)的操作。取出任何一本參考書,打開有關(guān)線程這一章:你能找到一個(gè)能在你的用戶界面中并行執(zhí)行數(shù)學(xué)運(yùn)算的多線程示例嗎?
我的目的不是讓你扔掉你的書,不要這樣做!多線程編程技術(shù)使基于用戶界面的應(yīng)用更完美。實(shí)際上, Microsoft .NET框架支持在任何語言編寫的窗口下應(yīng)用多線程編程技術(shù),允許開發(fā)人員設(shè)計(jì)非常豐富的界面,提供給終端用戶一個(gè)更好的體驗(yàn)。但是,多線程編程技術(shù)不僅僅是為了用戶界面的應(yīng)用,在沒有任何用戶界面的應(yīng)用中,一樣會(huì)出現(xiàn)多個(gè)執(zhí)行流的情況。
我們用一個(gè)“硬件商店”的客戶/服務(wù)器應(yīng)用系統(tǒng)作為例子??蛻舳耸鞘浙y機(jī),服務(wù)端是運(yùn)行在倉庫里一臺(tái)獨(dú)立的機(jī)器上的應(yīng)用系統(tǒng)。你可以想象一下,服務(wù)器沒有任何的用戶界面,如果不用多線程技術(shù)你將如何去實(shí)現(xiàn)?
服務(wù)端通過通道(http, sockets, files 等等)接收來自客戶端的請(qǐng)求并處理它們,然后發(fā)送一個(gè)應(yīng)答到客戶端。圖1顯示了它是如何運(yùn)作的。
圖1: 單線程的服務(wù)端應(yīng)用系統(tǒng)
為了讓客戶端的請(qǐng)求不會(huì)遺漏,服務(wù)端應(yīng)用系統(tǒng)實(shí)現(xiàn)了某種隊(duì)列來存放這些請(qǐng)求。圖1顯示了三個(gè)請(qǐng)求同時(shí)到達(dá),但只有其中的一個(gè)被服務(wù)端處理。當(dāng)服務(wù)端開始執(zhí)行 "Decrease stock of monkey wrench," 這個(gè)請(qǐng)求時(shí),其它兩個(gè)必須在隊(duì)列中等待。當(dāng)?shù)谝粋€(gè)執(zhí)行完成后,接著是第二個(gè),以此類推。這種方法普遍用于許多現(xiàn)有的系統(tǒng),但是這樣做系統(tǒng)的資源利用率很低。假設(shè) “decreasing the stock”請(qǐng)求修改磁盤上的一個(gè)文件,而這個(gè)文件正在被修改中,CPU將不會(huì)被使用,即使這個(gè)請(qǐng)求正處在待處理階段。這類系統(tǒng)的一個(gè)普遍特征就是低CPU利用時(shí)間導(dǎo)致出現(xiàn)很長(zhǎng)的響應(yīng)時(shí)間,甚至是在訪問壓力很大的環(huán)境里也這樣。
另外一個(gè)策略就是在當(dāng)前的系統(tǒng)中為每一個(gè)請(qǐng)求創(chuàng)建不同的線程。當(dāng)一個(gè)新的請(qǐng)求到達(dá)之后,服務(wù)端為進(jìn)入的請(qǐng)求創(chuàng)建一個(gè)新線程,執(zhí)行結(jié)束時(shí),再銷毀它。下圖說明了這個(gè)過程:
圖2:多線程服務(wù)端應(yīng)用系統(tǒng)
就像如圖2所示的那樣。我們有了較高的CPU利用率。即使它已經(jīng)不再像原來的那樣慢了,但創(chuàng)建線和銷毀程也不是最恰當(dāng)?shù)姆椒ā<僭O(shè)線程的執(zhí)行操作不復(fù)雜,由于需要花額外的時(shí)間去創(chuàng)建和銷毀線程,所以最終會(huì)嚴(yán)重影響系統(tǒng)的響應(yīng)時(shí)間。另外一點(diǎn)就是在壓力很大的環(huán)境下,這三個(gè)線程會(huì)給系統(tǒng)帶來很多的沖擊。多個(gè)線程同時(shí)執(zhí)行請(qǐng)求處理將導(dǎo)致CPU的利用率達(dá)到100%,而且大多數(shù)時(shí)間會(huì)浪費(fèi)在上下文切換過程中,甚至?xí)^處理請(qǐng)求的本身。這類系統(tǒng)的典型特征是大量的訪問會(huì)導(dǎo)致響應(yīng)時(shí)間呈指數(shù)級(jí)增長(zhǎng)和很高的CUP使用時(shí)間。
一個(gè)最優(yōu)的實(shí)現(xiàn)是綜合前面兩種方案而提出的觀點(diǎn)----線程池(Thread Pool),當(dāng)一個(gè)請(qǐng)求達(dá)到時(shí),應(yīng)用系統(tǒng)把置入接收隊(duì)列,一組的線程從隊(duì)列提取請(qǐng)求并處理之。這個(gè)方案如下圖所示:
圖3:?jiǎn)⒂镁€程池的服務(wù)端應(yīng)用系統(tǒng)
在這個(gè)例子中,我們用了一個(gè)含有兩個(gè)線程的線程池。當(dāng)三個(gè)請(qǐng)求到達(dá)時(shí),它們立刻安排到隊(duì)列等待被處理,因?yàn)閮蓚€(gè)線程都是空閑的,所以頭兩個(gè)請(qǐng)求開始執(zhí)行。當(dāng)其中任何一個(gè)請(qǐng)求處理結(jié)束后,空閑的線程就會(huì)去提取第三個(gè)請(qǐng)求并處理之。在這種場(chǎng)景中,系統(tǒng)不需要為每個(gè)請(qǐng)求創(chuàng)建和銷毀線程。線程之間能互相利用。而且如果線程池的執(zhí)行高效的話,它能增加或刪除線程以獲得最優(yōu)的性能。例如當(dāng)線程池在執(zhí)行兩個(gè)請(qǐng)求時(shí),而CPU的利用率才達(dá)到50%,這表明執(zhí)行請(qǐng)求正等待某個(gè)事件或者正在做某種I/O操作。線程池可以發(fā)現(xiàn)這種情況,并增加線程的數(shù)量以使系統(tǒng)能在同一時(shí)間處理更多的請(qǐng)求。相反的,如果CPU利用率達(dá)到100%,線程池可以減少線程的數(shù)量以獲得更多的CPU時(shí)間,而不要浪費(fèi)在上下文切換上面。
.NET中的線程池
基于上面的例子,在企業(yè)級(jí)應(yīng)用系統(tǒng)中有一個(gè)高效執(zhí)行的線程池是至關(guān)重要的。Microsoft在.NET框架的開發(fā)環(huán)境中已經(jīng)實(shí)現(xiàn)了這個(gè),該系統(tǒng)的核心提供了一個(gè)現(xiàn)成可用的最優(yōu)線程池。
這個(gè)線程池不僅對(duì)應(yīng)用程序可用,而且還融合到框架中的多數(shù)類中。.NET 建立在同一個(gè)池上是一個(gè)很重要的功能特性。比如 .NET Remoting 用它來處理來自遠(yuǎn)程對(duì)象的請(qǐng)求。
當(dāng)一個(gè)托管應(yīng)用程序開始執(zhí)行時(shí),運(yùn)行時(shí)環(huán)境(runtime)提供一個(gè)線程池,它將在代碼第一次訪問時(shí)被創(chuàng)建。這個(gè)池與應(yīng)用程序所在運(yùn)行的物理進(jìn)程關(guān)聯(lián)在一起,當(dāng)你用.NET框架下的同一進(jìn)程中運(yùn)行多個(gè)應(yīng)用程序的功能特性時(shí)(稱之為應(yīng)用程序域),這將是一個(gè)很重要的細(xì)節(jié)。在這種情況下,由于它們都使用同樣的線程池,一個(gè)壞的應(yīng)用程序會(huì)影響進(jìn)程中的其它應(yīng)用程序。
你可以通過System.Threading 名稱空間的Thread Pool 類來使用線程池,如果你查看一下這個(gè)類,就會(huì)發(fā)現(xiàn)所有的成員都是靜態(tài)的,而且沒有公開的構(gòu)造函數(shù)。這是有理由這樣做的,因?yàn)槊總€(gè)進(jìn)程只有一個(gè)線程池,并且我們不能創(chuàng)建新的。這個(gè)限制的目的是為了把所有的異步編程技術(shù)都集中到同一個(gè)池中。所以我們不能擁有一個(gè)通過第三方組建創(chuàng)建的無法管理的線程池。
ThreadPool.QueueUserWorkItem 方法運(yùn)行我們?cè)谙到y(tǒng)線程池上啟動(dòng)一個(gè)函數(shù),它的聲明如下:
public static bool QueueUserWorkItem (WaitCallback callBack, object state)
第一個(gè)參數(shù)指明我們將在池中執(zhí)行的函數(shù),它的聲明必須與WaitCallback代理(delegate)互相匹配:public delegate void WaitCallback (object state);
State 參數(shù)允許任何類型的信息傳遞到該方法中,它在調(diào)用QueueUserWorkItem時(shí)傳入。
讓我們結(jié)合這些新概念,看看“硬件商店”的另一個(gè)實(shí)現(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");
}
}
}
為了簡(jiǎn)化例子,我們?cè)?/span>Main 類中創(chuàng)建一個(gè)靜態(tài)方法用于處理請(qǐng)求。由于代理的靈活性,我們可以指定任何實(shí)例方法去處理請(qǐng)求,只要這些方法的聲明與代理相同。在這里范例中,通過調(diào)用Thread.Sleep,實(shí)現(xiàn)延遲兩秒以模擬處理時(shí)間。
你如果編譯和執(zhí)行這個(gè)范例,將會(huì)看到下面的輸出:
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
注意,所有的請(qǐng)求都被不同的線程并行處理了。
我們可以通過在兩個(gè)方法中加入如下的代碼,以此看到更多的信息。
// 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());
我們?cè)黾恿艘粋€(gè)Thread.CurrentThread.IsThreadPoolThread的調(diào)用。如果目標(biāo)線程屬于線程池,這個(gè)屬性將返回True。另外,我們還顯示了用GetHashCode 方法從當(dāng)前線程返回的結(jié)果。它是唯一標(biāo)識(shí)當(dāng)前執(zhí)行線程的值?,F(xiàn)在看一看這個(gè)輸出結(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
你可以看到所有的請(qǐng)求都被系統(tǒng)線程池中的不同線程執(zhí)行。再次運(yùn)行這個(gè)例子,注意系統(tǒng)CPU的利用率,如果你沒有任何其它應(yīng)用程序在后臺(tái)運(yùn)行的話,它幾乎是0%。因?yàn)橄到y(tǒng)唯一正在做的是每執(zhí)行2秒后就掛起的處理。
我們來修改一下這個(gè)應(yīng)用,這次我們不掛起處理請(qǐng)求的線程,相反我們會(huì)一直讓系統(tǒng)忙,為了做到這點(diǎn),我們用Environment.TickCount. 構(gòu)建一個(gè)每隔兩秒就對(duì)請(qǐng)求執(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
注意第三個(gè)請(qǐng)求是在第一個(gè)請(qǐng)求處理結(jié)束之后執(zhí)行的,而且線程的號(hào)碼仍然用原來的7,這個(gè)原因是線程池檢測(cè)到CPU的使用率已經(jīng)達(dá)到100%,一直等待某個(gè)線程空閑。它并不會(huì)重新創(chuàng)建一個(gè)新的線程,這樣就會(huì)減少線程間的上下文切換開銷,以使總體性能更佳。
假如你曾經(jīng)開發(fā)過Microsoft Win32的應(yīng)用程序,你知道SetTimer函數(shù)是API之一,通過這個(gè)函數(shù)可以指定的一個(gè)窗口接收到來自系統(tǒng)時(shí)間周期的WM_TIMER消息。用這個(gè)方法遇到的第一個(gè)問題是你需要一個(gè)窗口去接收消息,所以你不能用在控制臺(tái)應(yīng)用程序中。另外,基于消息的實(shí)現(xiàn)并不是非常精確,假如你的應(yīng)用程序正在處理其它消息,情況有可能更糟糕。
相對(duì)基于Win32的定時(shí)器來說, .NET 中一個(gè)很重要的改進(jìn)就是創(chuàng)建不同的線程,該線程阻塞指定的時(shí)間,然后通知一個(gè)回調(diào)函數(shù)。這里的定時(shí)器不需要Microsoft的消息系統(tǒng),所以這樣就更精確,而且還能用于控制臺(tái)應(yīng)用程序中。以下代碼顯示了這個(gè)技術(shù)的一種實(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");
}
}
這個(gè)代碼一般用于Wn32應(yīng)用中。每個(gè)定時(shí)器創(chuàng)建獨(dú)立的線程,并且等待指定的時(shí)間,然后呼叫回調(diào)函數(shù)。猶如你看到的那樣,這個(gè)實(shí)現(xiàn)的成本會(huì)非常高。如果你的應(yīng)用程序使用了多個(gè)定時(shí)器,相對(duì)的線程數(shù)量也會(huì)隨著使用定時(shí)器的數(shù)量而增長(zhǎng)。
現(xiàn)在我們有.NET 提供的線程池,我們可以從池中改變請(qǐng)求的等待函數(shù),這樣就十分有效,而且會(huì)提升系統(tǒng)的性能。我們會(huì)遇到兩個(gè)問題:
n 假如線程池已滿(所有的線程都在運(yùn)行中),那么這個(gè)請(qǐng)求排到隊(duì)列中等待,而且定時(shí)器不在精確。
n 假如創(chuàng)建了多個(gè)定時(shí)器,線程池會(huì)因?yàn)榈却鼈儠r(shí)間片失效而非常忙。
為了避免這些問題,.NET框架的線程池提供了獨(dú)立于時(shí)間的請(qǐng)求。用了這個(gè)函數(shù),我們可以不用任何線程就可以擁有成千上萬個(gè)定時(shí)器,一旦時(shí)間片失效,這時(shí),線程池將會(huì)處理這些請(qǐng)求。
這些特色出現(xiàn)在兩個(gè)不同的類中:
System.Threading.Timer
定時(shí)器的簡(jiǎn)單版本,它運(yùn)行開發(fā)人員向線程池中的定期執(zhí)行的程序指定一個(gè)代理(delegate).
System.Timers.Timer
System.Threading.Timer的組件版本,允許開發(fā)人員把它拖放到一個(gè)窗口表單(form)中,可以把一個(gè)事件作為執(zhí)行的函數(shù)。
這非常有助于理解上述兩個(gè)類與另外一個(gè)稱為System.Windows.Forms.Timer.的類。這個(gè)類只是封裝了Win32中消息機(jī)制的計(jì)數(shù)器,如果你不準(zhǔn)備開發(fā)多線程應(yīng)用,那么就可以用這個(gè)類。
在下面的例子中,我們將用System.Threading.Timer 類,定時(shí)器的最簡(jiǎn)單實(shí)現(xiàn),我們只需要如下定義的構(gòu)造方法
public Timer(TimerCallback callback,
object state,
int dueTime,
int period);
對(duì)于第一個(gè)參數(shù)(callback),我們可以指定定時(shí)執(zhí)行的函數(shù);第二個(gè)參數(shù)是傳遞給函數(shù)的通用對(duì)象;第三個(gè)參數(shù)是計(jì)時(shí)器開始執(zhí)行前的延時(shí);最后一個(gè)參數(shù)period,是兩個(gè)執(zhí)行之間的毫秒數(shù)。
下面的例子創(chuàng)建了兩個(gè)定時(shí)器,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
猶如你看到的那樣,兩個(gè)定時(shí)器中的所有函數(shù)調(diào)用都在同一個(gè)線程中執(zhí)行(ID = 2),應(yīng)用程序使用的資源最小化了。
相對(duì)于定時(shí)器,.NET線程池允許在執(zhí)行函數(shù)上同步對(duì)象,為了在多線程環(huán)境中的各線程之間共享資源,我們需要用.NET同步對(duì)象。
如果我們沒有線程,或者線程必須阻塞直到事件收到信號(hào),就像我前面提到一樣,這會(huì)增加應(yīng)用程序中總的線程數(shù)量,結(jié)果導(dǎo)致系統(tǒng)需要更多的資源和CPU時(shí)間。
線程池允許我們把請(qǐng)求進(jìn)行排隊(duì),直到某個(gè)特殊的同步對(duì)象收到信號(hào)后執(zhí)行。如果這個(gè)信號(hào)沒有收到,請(qǐng)求函數(shù)將不需要任何線程,所以可以保證系統(tǒng)性能最優(yōu)化。ThreadPool類提供了下面的方法:
public static RegisteredWaitHandle RegisterWaitForSingleObject(
WaitHandle waitObject,
WaitOrTimerCallback callBack,
object state,
int millisecondsTimeOutInterval,
bool executeOnlyOnce);
第一個(gè)參數(shù),waitObject 可以是任何繼承于WaitHandle的對(duì)象:
Mutex
ManualResetEvent
AutoResetEvent
就像你看到的那樣,只有系統(tǒng)的同步對(duì)象才能用在這里,就是繼承自WaitHandle的對(duì)象。你不能用其它任何的同步機(jī)制,比如moniter 或者 read-write 鎖。剩余的參數(shù)允許我們指明當(dāng)一個(gè)對(duì)象收到信號(hào)后執(zhí)行的函數(shù)(callBack);一個(gè)傳遞給函數(shù)的狀態(tài)(state); 線程池等待對(duì)象的最大時(shí)間 (millisecondsTimeOutInterval) 和一個(gè)標(biāo)識(shí)表明對(duì)象收到信號(hào)時(shí)函數(shù)只能執(zhí)行一次, (executeOnlyOnce). 下面的代理聲明目的是用在函數(shù)的回調(diào):
delegate void WaitOrTimerCallback(
object state,
bool timedOut);
如果參數(shù) timeout 設(shè)置的最大時(shí)間已經(jīng)失效,但是沒有同步對(duì)象收到信號(hào)的花,這個(gè)函數(shù)就會(huì)被調(diào)用。
下面的例子用了一個(gè)手工事件和一個(gè)互斥量來通知線程池中的執(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é)束顯示兩個(gè)函數(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)用場(chǎng)景就是I/O操作。多數(shù)應(yīng)用系統(tǒng)需要讀磁盤,數(shù)據(jù)發(fā)送到Sockets,因特網(wǎng)連接等等。所有的這些操作都有一些特征,直到他們執(zhí)行操作時(shí),才需要CPU時(shí)間。.NET 框架為所有這些可能執(zhí)行的異步操作提供了I/O類。當(dāng)這些操作執(zhí)行完后,線程池中特定的函數(shù)會(huì)執(zhí)行。尤其是在服務(wù)器應(yīng)用程序中執(zhí)行多線程異步操作,性能會(huì)更好。
在第一個(gè)例子中,我們將把一個(gè)文件異步寫到硬盤中??匆豢?/span>FileStream 的構(gòu)造方法是如何使用的:
public FileStream(
string path,
FileMode mode,
FleAccess access,
FleShare share,
int bufferSize,
bool useAsync);
最后一個(gè)參數(shù)非常有趣,我們應(yīng)該對(duì)異步執(zhí)行文件的操作設(shè)置useAsync為True。如果我們沒有這樣做,即使我們用了異步函數(shù),它們的操作仍然會(huì)被主叫線程阻塞。
下面的例子說明了用一旦FileStream BeginWrite方法寫文件操作結(jié)束,線程池中的一個(gè)回調(diào)函數(shù)將會(huì)被執(zhí)行。注意我們可以在任何時(shí)候訪問IAsyncResult接口,它可以用來了解當(dāng)前操作的狀態(tài)。我們可以用CompletedSynchronously 屬性指示一個(gè)異步操作是否完成,而當(dāng)一個(gè)操作結(jié)束時(shí),IsCompleted 屬性會(huì)設(shè)上一個(gè)值。IAsyncResult 提供了很多有趣的屬性,比如:AsyncWaitHandle ,一旦操作完成,一個(gè)異步對(duì)象將會(huì)被通知。
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的場(chǎng)景中,由于I/O操作通常比磁盤操作慢,這時(shí)用線程池就顯得尤為重要。過程跟前面提到的差不多,Socket 類提供了多個(gè)方法用于執(zhí)行異步操作:
BeginRecieve
BeginSend
BeginConnect
BeginAccept
假如你的服務(wù)器應(yīng)用使用了Socket來與客戶端通訊,一定會(huì)用到這些方法。這種方法取代了對(duì)每個(gè)客戶端連接都啟用一個(gè)線程的做法,所有的操作都在線程池中異步執(zhí)行。
下面的例子用另外一個(gè)支持異步操作的類,HttpWebRequest。用這個(gè)類,我們可以建立一個(gè)到Web服務(wù)器的連接。這個(gè)方法叫BeginGetResponse, 但在這個(gè)例子中有一個(gè)很重要的區(qū)別。在上面最后一個(gè)示例中,我們沒有用到從操作中返回的結(jié)果。但是,我們現(xiàn)在需要當(dāng)一個(gè)操作結(jié)束時(shí)從Web服務(wù)器返回的響應(yīng),為了接收到這個(gè)信息,.NET中所有提供異步操作的類都提供了成對(duì)的方法。在HttpWebRequest這個(gè)類中,這個(gè)成對(duì)的方法就是:BeginGetResponse 和EndGetResponse。用了End版本,我們可以接收操作的結(jié)果。在我們的示例中,EndGetResponse 會(huì)從Web服務(wù)器接收響應(yīng)。
雖然可以在任何時(shí)間調(diào)用EndGetResponse 方法,但在我們的例子中是在回調(diào)函數(shù)中做的。僅僅是因?yàn)槲覀兿胫酪呀?jīng)做了異步請(qǐng)求。如果我們?cè)谥罢{(diào)用EndGetResponse ,這個(gè)調(diào)用將一直阻塞到操作完成。
在下面的例子中,我們發(fā)送一個(gè)請(qǐng)求到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
過了一會(huì)兒,響應(yīng)接收到了。下面的結(jié)果顯示:
Response received on pool: True
Response size: 27331
就像你看到的那樣,一旦收到響應(yīng),線程池的異步函數(shù)就會(huì)執(zhí)行。
ThreadPool 類提供了兩個(gè)方法用來查詢線程池的狀態(tài)。第一個(gè)是我們可以從線程池獲取當(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í)行的。顯式的用戶請(qǐng)求(QueueUserWorkItem方法),基于異步對(duì)象的方法(RegisterWaitForSingleObject)和定時(shí)器(Timer類)
CompletionPortThreads
這種線程常常用來I/O操作,Windows NT, Windows 2000 和 Windows XP提供了一個(gè)步執(zhí)行的對(duì)象,叫做IOCompletionPort。把API和異步對(duì)象關(guān)聯(lián)起來,用少量的資源和有效的方法,我們就可以調(diào)用系統(tǒng)線程池的異步I/O操作。但是在Windows 95, Windows 98, 和 Windows Me有一些局限。比如: 在某些設(shè)備上,沒有提供IOCompletionPorts 功能和一些異步操作,如磁盤和郵件槽。在這里你可以看到.NET框架的最大特色:一次編譯,可以在多個(gè)系統(tǒng)下運(yùn)行。根據(jù)不同的目標(biāo)平臺(tái),.NET 框架會(huì)決定是否使用IOCompletionPorts API,用最少的資源達(dá)到最好的性能。
這節(jié)包含一個(gè)使用Socket 類的例子。在這個(gè)示例中,我們將異步建立一個(gè)連接到本地的Web服務(wù)器,然后發(fā)送一個(gè)Get請(qǐng)求。通過這個(gè)例子,我們可以很容易地鑒別這兩種不同的線程。
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)行這個(gè)程序,你將會(huì)看到如下結(jié)果:
Connected to localhost:80
WorkerThreads: 24, CompletionPortThreads: 25
Request sent to localhost:80
WorkerThreads: 25, CompletionPortThreads: 24
如你所看到地那樣,連接用了工作線程,而發(fā)送數(shù)據(jù)用了一個(gè)完成端口(CompletionPort),接著看下面的順序:
1. 我們得到一個(gè)本地IP地址,然后異步連接到那里。
2. Socket在工作線程上執(zhí)行異步連接操作,因?yàn)樵?/span>Socket上,不能用Windows 的IOCompletionPorts來建立連接。
3. 一旦連接建立了,Socket類調(diào)用指明的函數(shù)ConnectCallback,這個(gè)回調(diào)函數(shù)顯示了線程池中可用的線程數(shù)量。我們可以看到這些是在工作線程中執(zhí)行的。
4. 在用ASCII碼對(duì)Get請(qǐng)求進(jìn)行編碼后,我們用BeginSend方法從同樣的函數(shù)ConnectCallback 中發(fā)送一個(gè)異步請(qǐng)求。
5. Socket上的發(fā)送和接收操作可以通過IOCompletionPort 來執(zhí)行異步操作,所以當(dāng)請(qǐng)求做完后,回調(diào)函數(shù)就會(huì)在一個(gè)CompletionPort類型的線程中執(zhí)行。因?yàn)楹瘮?shù)本身顯示了可用的線程數(shù)量,所以我們可以通過這個(gè)來查看,對(duì)應(yīng)的完成端口數(shù)已經(jīng)減少了多少。
如果我們?cè)?/span>Windows 95, Windows 98, 或者 Windows Me平臺(tái)上運(yùn)行相同的代碼,會(huì)出現(xiàn)相同的連接結(jié)果,請(qǐng)求將被發(fā)送到工作線程,而非完成端口。你應(yīng)該知道的很重要的一點(diǎn)就是,Socket類總是會(huì)利用最優(yōu)的可用機(jī)制,所以你在開發(fā)應(yīng)用時(shí),可以不用考慮目標(biāo)平臺(tái)是什么。
你已經(jīng)看到在上面的例子中每種類型的線程可用的最大數(shù)是25。我們可以用GetMaxThreads返回這個(gè)值:
public static void GetMaxThreads(
out int workerThreads,
out int completionPortThreads);
一旦到了最大的數(shù)量,就不會(huì)創(chuàng)建新線程,所有的請(qǐng)求都將被排隊(duì)。假如你看過ThreadPool類的所有方法,你將發(fā)現(xiàn)沒有一個(gè)允許我們更改最大數(shù)的方法。就像我們前面提到的那樣,線程池是每個(gè)處理過程的唯一共享資源。這就是為什么不可能讓應(yīng)用程序域去更改這個(gè)配置的原因。想象一下出現(xiàn)這種情況的后果,如果有第三方組件把線程池中線程的最大數(shù)改為1,整個(gè)應(yīng)用都會(huì)停止工作,甚至在進(jìn)程中其它的應(yīng)用程序域都將受到影響。同樣的原因,公共語言運(yùn)行時(shí)的宿主也有可能去更改這個(gè)配置。比如:ASP.NET允許系統(tǒng)管理員更改這個(gè)數(shù)字。
在你的應(yīng)用程序使用線程池之前,還有一個(gè)東西你應(yīng)該知道:死鎖。在線程池中執(zhí)行一個(gè)實(shí)現(xiàn)不好的異步對(duì)象可能導(dǎo)致你的整個(gè)應(yīng)用系統(tǒng)中止運(yùn)行。
設(shè)想你的代碼中有個(gè)方法,它需要通過Socket連接到一個(gè)Web服務(wù)器上。一個(gè)可能的實(shí)現(xiàn)就是用Socket 類中的BeginConnect方法異步打開一個(gè)連接,然后用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一直阻塞到連接被建立。
如果線程池中的一個(gè)執(zhí)行函數(shù)中用了這個(gè)類的方法,將會(huì)發(fā)生什么事情呢?設(shè)想線程池的大小只有兩個(gè)線程,然后用我們的連接類創(chuàng)建了兩個(gè)異步對(duì)象。當(dāng)這兩個(gè)函數(shù)同時(shí)在池中執(zhí)行時(shí),線程池已經(jīng)沒有用于其它請(qǐng)求的空間了,除非直到某個(gè)函數(shù)結(jié)束。問題是這些函數(shù)調(diào)用了我們類中的Connect方法,這個(gè)方法在線程池中又發(fā)起了一個(gè)異步操作。但線程池一直是滿的,所以請(qǐng)求就一直等待任何空閑線程的出現(xiàn)。不幸的是,這將永遠(yuǎn)不會(huì)發(fā)生,因?yàn)槭褂镁€程池的函數(shù)正等待隊(duì)列函數(shù)的結(jié)束。結(jié)論就是:我們的應(yīng)用系統(tǒng)已經(jīng)阻塞了。
我們以此推斷25個(gè)線程的線程池的行為。假如25個(gè)函數(shù)都等待異步對(duì)象操作的結(jié)束。結(jié)果將是一樣的,死鎖一樣會(huì)出現(xiàn)。
在下面的代碼片斷中,我們使用了這個(gè)類來說明問題:
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)行這個(gè)例子,你將看到池中的線程是如何把線程的可用數(shù)量減少到零的,接著應(yīng)用中止,死鎖出現(xiàn)了。
如果你想在你的應(yīng)用中避免出現(xiàn)死鎖,永遠(yuǎn)不要阻塞正在等待線程池中的其它函數(shù)的線程。這看起來很容易,但記住這個(gè)規(guī)則意味著有兩條:
n 不要?jiǎng)?chuàng)建這樣的類,它的同步方法在等待異步函數(shù)。因?yàn)檫@種類可能被線程池中的線程調(diào)用。
n 不要在任何異步函數(shù)中使用這樣的類,如果它正等待著這個(gè)異步函數(shù)。
如果你想檢測(cè)到應(yīng)用中的死鎖情況,那么就當(dāng)你的系統(tǒng)掛起時(shí),檢查線程池中的線程可用數(shù)。線程的可用數(shù)量已經(jīng)沒有并且CPU的使用率為0 ,這是很明顯的死鎖癥狀。你應(yīng)該檢查你的代碼,以確定哪個(gè)在線程中執(zhí)行的函數(shù)正在等待異步操作,然后刪除它。
如果你再看看ThreadPool類,你會(huì)看到有兩個(gè)方法我們沒有用到,UnsafeQueueUserWorkItem 和UnsafeRegisterWaitForSingleObject。 為了完全理解這些方法,首先,我們必須回憶 .NET框架中安全策略是怎么運(yùn)作的。
Windows安全機(jī)制是關(guān)注資源。操作系統(tǒng)本身允許對(duì)文件,用戶,注冊(cè)表鍵值和任何其它的系統(tǒng)資源設(shè)定權(quán)限。這種方法對(duì)應(yīng)用系統(tǒng)的用戶認(rèn)證非常有效,但當(dāng)出現(xiàn)用戶對(duì)他使用的系統(tǒng)產(chǎn)生不信任的情況時(shí),這就會(huì)有些局限性。例如這些程序是從Internet下載的。在這種情況下,一旦用戶安裝了這個(gè)程序,它就可以執(zhí)行用戶權(quán)限范圍內(nèi)的任何操作。舉個(gè)例子,假如用戶可以刪除他公司內(nèi)的任何共享文件,任何從Internet下載的程序也都可以這樣做。
.NET 提供了應(yīng)用到程序的安全性策略,而不是用戶。這就是說,在用戶權(quán)限的范圍內(nèi),我們可以限制任何執(zhí)行單元(程序集)使用的資源。通過MMC,我們可以根據(jù)條件定義一組程序集,然后為每組設(shè)置不同的策略,一個(gè)典型的例子就是限制從Internet下載的程序訪問磁盤的權(quán)限。
為了讓這個(gè)功能運(yùn)轉(zhuǎn)起來,.NET 框架必須維護(hù)一個(gè)不同程序集之間的調(diào)用棧。假設(shè)一個(gè)應(yīng)用沒有權(quán)限訪問磁盤,但是它調(diào)用了一個(gè)對(duì)整個(gè)系統(tǒng)都可以訪問的類庫,當(dāng)?shù)诙€(gè)程序集執(zhí)行一個(gè)磁盤的操作時(shí),設(shè)置到這個(gè)程序集的權(quán)限允許這樣做,但是權(quán)限不會(huì)被應(yīng)用到主叫程序集,.NET不僅要檢查當(dāng)前程序集的權(quán)限,而且會(huì)檢查整個(gè)調(diào)用棧的權(quán)限。這個(gè)棧已經(jīng)被高度優(yōu)化了,但是它們給兩個(gè)不同程序集之間的調(diào)用增加了額外的負(fù)擔(dān)。
UnsafeQueueUserWorkItem , UnsafeRegisterWaitForSingleObject與 QueueUserWorkItem , RegisterWaitForSingleObject兩個(gè)方法類似。由于是非安全版本不會(huì)維護(hù)它們執(zhí)行函數(shù)之間的調(diào)用棧,所以非安全版本運(yùn)行的更快些。但是回調(diào)函數(shù)將只在當(dāng)前程序集的安全策略下執(zhí)行,它就不能應(yīng)用權(quán)限到整個(gè)調(diào)用棧中的程序集。
我的建議是僅在性能非常重要的、安全已經(jīng)控制好的極端情況下才用非安全版本。例如,你構(gòu)建的應(yīng)用程序不會(huì)被其它的程序集調(diào)用,或者僅被很明確清楚的程序集使用,那么你可以用非安全版本。如果你開發(fā)的類庫會(huì)被第三方應(yīng)用程序中使用,那么你就不應(yīng)該用這些方法,因?yàn)樗鼈兛赡苡媚愕膸飓@取訪問系統(tǒng)資源的權(quán)限。
在下面例子中,你可以看到用UnsafeQueueUserWorkItem方法的風(fēng)險(xiǎn)。我們將構(gòu)建兩個(gè)單獨(dú)的程序集,在第一個(gè)程序集中我們將在線程池中創(chuàng)建一個(gè)文件,然后我們將導(dǎo)出一個(gè)類以使這個(gè)操作可以被其它的程序集執(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");
}
}
}
第二個(gè)程序集引用了第一個(gè),并且用了CheckIt 方法去創(chuàng)建一個(gè)文件:
using System;
namespace ThreadSecurityTest
{
class MainApp
{
static void Main ()
{
PoolCheck pc = new PoolCheck();
pc.CheckIt();
Console.ReadLine();
}
}
}
編譯這兩個(gè)程序集,然后運(yùn)行main應(yīng)用。默認(rèn)情況下,你的應(yīng)用被配置為允許執(zhí)行磁盤操作,所以系統(tǒng)成功生成文件。
File created
現(xiàn)在,打開.NET框架的配置。為了簡(jiǎn)化這個(gè)例子,我們僅創(chuàng)建一個(gè)代碼組關(guān)聯(lián)到main應(yīng)用。接著展開 運(yùn)行庫安全策略/ 計(jì)算機(jī)/ 代碼組/ All_Code /,增加一個(gè)叫ThreadSecurityTest的組。在向?qū)е校x擇Hash 條件并導(dǎo)入Hash到我們的應(yīng)用中,設(shè)置為Internet 級(jí)別,并選擇“該策略級(jí)別將只具有與此代碼組關(guān)聯(lián)的權(quán)限集中的權(quán)限”選項(xiàng)。
運(yùn)行應(yīng)用程序,看看會(huì)發(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=b77a 5c 561934e089 failed.
我們的策略開始工作,系統(tǒng)已經(jīng)不能創(chuàng)建文件了。這是因?yàn)?/span>.NET框架為我們維護(hù)了一個(gè)調(diào)用棧才使它成為了可能,雖然創(chuàng)建文件的庫有權(quán)限去訪問系統(tǒng)。
現(xiàn)在把庫中的QueueUserWorkItem替換為UnsafeQueueUserWorkItem,再次編譯程序集,然后運(yùn)行Main程序?,F(xiàn)在的結(jié)果是:
File created
即使我們的系統(tǒng)沒有足夠的權(quán)限去訪問磁盤,但我們已經(jīng)創(chuàng)建了一個(gè)向整個(gè)系統(tǒng)公開它的功能的庫,卻沒有維護(hù)它的調(diào)用棧。記住一個(gè)金牌規(guī)則: 僅在你的代碼不允許讓其它的應(yīng)用系統(tǒng)調(diào)用,或者當(dāng)你想要嚴(yán)格限制訪問很明確清楚的程序集,才使用非安全的函數(shù)。
在這篇文章中,我們知道了為什么在我們的服務(wù)器應(yīng)用中需要使用線程池來優(yōu)化資源和CPU的利用。我們學(xué)習(xí)了一個(gè)線程池是如何實(shí)現(xiàn)的,需要考慮多個(gè)因素如:CPU使用的百分比,隊(duì)列請(qǐng)求或者系統(tǒng)的處理器數(shù)量。
.NET提供了豐富的線程池的功能以讓我們的應(yīng)用程序使用, 并且與.NET框架的類緊密地集成在一起。這個(gè)線程池是高度優(yōu)化了的,它只需要最少的CPU時(shí)間和資源,而且總能適應(yīng)目標(biāo)平臺(tái)。
因?yàn)榕c框架集成在一起,所以框架中的大部分類都提供了使用線程池的內(nèi)在功能,給開發(fā)人員提供了集中管理和監(jiān)視應(yīng)用中的線程池的功能。鼓勵(lì)第三方組件使用線程池,這樣它們的客戶就可以享受.NET所提供的全部功能。允許執(zhí)行用戶函數(shù),定時(shí)器,I/O操作和同步對(duì)象。
假如你在開發(fā)服務(wù)器應(yīng)用系統(tǒng),只要有可能就在你的請(qǐng)求處理系統(tǒng)中使用線程池?;蛘吣汩_發(fā)了一個(gè)讓服務(wù)器程序使用的庫,那么盡可能提供系統(tǒng)線程池的異步對(duì)象處理。
聯(lián)系客服