.NET線程池

摘要

深度探索?Microsoft .NET提供的線程池,?揭示什么情況下你需要用線程池以及?.NET框架下的線程池是如何實現的,并告訴你如何去使用線程池。

?

內容

介紹

.NET中的線程池

線程池中執行的函數

使用定時器

同步對象的執行

異步I/O操作

監視線程池

死鎖

有關安全性

結束

?

介紹

?????????如果你有在任何編程語言下的多線程編程經驗的話,你肯定已經非常熟悉一些典型的范例。通常,多線程編程與基于用戶界面的應用聯系在一起,它們需要在不影響終端用戶的情況下,執行一些耗時的操作。取出任何一本參考書,打開有關線程這一章:你能找到一個能在你的用戶界面中并行執行數學運算的多線程示例嗎?

我的目的不是讓你扔掉你的書,不要這樣做!多線程編程技術使基于用戶界面的應用更完美。實際上,?Microsoft .NET框架支持在任何語言編寫的窗口下應用多線程編程技術,允許開發人員設計非常豐富的界面,提供給終端用戶一個更好的體驗。但是,多線程編程技術不僅僅是為了用戶界面的應用,在沒有任何用戶界面的應用中,一樣會出現多個執行流的情況。

我們用一個“硬件商店”的客戶/服務器應用系統作為例子。客戶端是收銀機,服務端是運行在倉庫里一臺獨立的機器上的應用系統。你可以想象一下,服務器沒有任何的用戶界面,如果不用多線程技術你將如何去實現?

服務端通過通道(http, sockets, files?等等)接收來自客戶端的請求并處理它們,然后發送一個應答到客戶端。圖1顯示了它是如何運作的。

?

圖1:?單線程的服務端應用系統

為了讓客戶端的請求不會遺漏,服務端應用系統實現了某種隊列來存放這些請求。圖1顯示了三個請求同時到達,但只有其中的一個被服務端處理。當服務端開始執行?"Decrease stock of monkey wrench,"?這個請求時,其它兩個必須在隊列中等待。當第一個執行完成后,接著是第二個,以此類推。這種方法普遍用于許多現有的系統,但是這樣做系統的資源利用率很低。假設?“decreasing the stock”請求修改磁盤上的一個文件,而這個文件正在被修改中,CPU將不會被使用,即使這個請求正處在待處理階段。這類系統的一個普遍特征就是低CPU利用時間導致出現很長的響應時間,甚至是在訪問壓力很大的環境里也這樣。

?????????另外一個策略就是在當前的系統中為每一個請求創建不同的線程。當一個新的請求到達之后,服務端為進入的請求創建一個新線程,執行結束時,再銷毀它。下圖說明了這個過程:

?

?????????圖2:多線程服務端應用系統

就像如圖2所示的那樣。我們有了較高的CPU利用率。即使它已經不再像原來的那樣慢了,但創建線和銷毀程也不是最恰當的方法。假設線程的執行操作不復雜,由于需要花額外的時間去創建和銷毀線程,所以最終會嚴重影響系統的響應時間。另外一點就是在壓力很大的環境下,這三個線程會給系統帶來很多的沖擊。多個線程同時執行請求處理將導致CPU的利用率達到100%,而且大多數時間會浪費在上下文切換過程中,甚至會超過處理請求的本身。這類系統的典型特征是大量的訪問會導致響應時間呈指數級增長和很高的CUP使用時間。

?????????一個最優的實現是綜合前面兩種方案而提出的觀點----線程池(Thread Pool),當一個請求達到時,應用系統把置入接收隊列,一組的線程從隊列提取請求并處理之。這個方案如下圖所示:

?

?

圖3:啟用線程池的服務端應用系統

在這個例子中,我們用了一個含有兩個線程的線程池。當三個請求到達時,它們立刻安排到隊列等待被處理,因為兩個線程都是空閑的,所以頭兩個請求開始執行。當其中任何一個請求處理結束后,空閑的線程就會去提取第三個請求并處理之。在這種場景中,系統不需要為每個請求創建和銷毀線程。線程之間能互相利用。而且如果線程池的執行高效的話,它能增加或刪除線程以獲得最優的性能。例如當線程池在執行兩個請求時,而CPU的利用率才達到50%,這表明執行請求正等待某個事件或者正在做某種I/O操作。線程池可以發現這種情況,并增加線程的數量以使系統能在同一時間處理更多的請求。相反的,如果CPU利用率達到100%,線程池可以減少線程的數量以獲得更多的CPU時間,而不要浪費在上下文切換上面。

?

.NET中的線程池

?????????基于上面的例子,在企業級應用系統中有一個高效執行的線程池是至關重要的。Microsoft在.NET框架的開發環境中已經實現了這個,該系統的核心提供了一個現成可用的最優線程池。

這個線程池不僅對應用程序可用,而且還融合到框架中的多數類中。.NET?建立在同一個池上是一個很重要的功能特性。比如?.NET Remoting?用它來處理來自遠程對象的請求。

?????????當一個托管應用程序開始執行時,運行時環境(runtime)提供一個線程池,它將在代碼第一次訪問時被創建。這個池與應用程序所在運行的物理進程關聯在一起,當你用.NET框架下的同一進程中運行多個應用程序的功能特性時(稱之為應用程序域),這將是一個很重要的細節。在這種情況下,由于它們都使用同樣的線程池,一個壞的應用程序會影響進程中的其它應用程序。

?????????你可以通過System.Threading?名稱空間的Thread Pool?類來使用線程池,如果你查看一下這個類,就會發現所有的成員都是靜態的,而且沒有公開的構造函數。這是有理由這樣做的,因為每個進程只有一個線程池,并且我們不能創建新的。這個限制的目的是為了把所有的異步編程技術都集中到同一個池中。所以我們不能擁有一個通過第三方組建創建的無法管理的線程池。

?

線程池中執行的函數

ThreadPool.QueueUserWorkItem?方法運行我們在系統線程池上啟動一個函數,它的聲明如下:

public static bool QueueUserWorkItem (WaitCallback callBack, object state)
第一個參數指明我們將在池中執行的函數,它的聲明必須與WaitCallback代理(delegate)互相匹配:public delegate void WaitCallback (object state);

State?參數允許任何類型的信息傳遞到該方法中,它在調用QueueUserWorkItem時傳入。

?

讓我們結合這些新概念,看看“硬件商店”的另一個實現。

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?類中創建一個靜態方法用于處理請求。由于代理的靈活性,我們可以指定任何實例方法去處理請求,只要這些方法的聲明與代理相同。在這里范例中,通過調用Thread.Sleep,實現延遲兩秒以模擬處理時間。

你如果編譯和執行這個范例,將會看到下面的輸出:

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的調用。如果目標線程屬于線程池,這個屬性將返回True。另外,我們還顯示了用GetHashCode?方法從當前線程返回的結果。它是唯一標識當前執行線程的值。現在看一看這個輸出結果:

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
?

你可以看到所有的請求都被系統線程池中的不同線程執行。再次運行這個例子,注意系統CPU的利用率,如果你沒有任何其它應用程序在后臺運行的話,它幾乎是0%。因為系統唯一正在做的是每執行2秒后就掛起的處理。

?????????我們來修改一下這個應用,這次我們不掛起處理請求的線程,相反我們會一直讓系統忙,為了做到這點,我們用Environment.TickCount.?構建一個每隔兩秒就對請求執行一次的循環。

int ticks = Environment.TickCount;
while(Environment.TickCount - ticks < 2000);

現在打開任務管理器,看一看CPU的使用率,你將看到應用程序占有了CPU的100%的使用率。再看一下我們程序的輸出結果:

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
?

注意第三個請求是在第一個請求處理結束之后執行的,而且線程的號碼仍然用原來的7,這個原因是線程池檢測到CPU的使用率已經達到100%,一直等待某個線程空閑。它并不會重新創建一個新的線程,這樣就會減少線程間的上下文切換開銷,以使總體性能更佳。

?

使用定時器

假如你曾經開發過Microsoft Win32的應用程序,你知道SetTimer函數是API之一,通過這個函數可以指定的一個窗口接收到來自系統時間周期的WM_TIMER消息。用這個方法遇到的第一個問題是你需要一個窗口去接收消息,所以你不能用在控制臺應用程序中。另外,基于消息的實現并不是非常精確,假如你的應用程序正在處理其它消息,情況有可能更糟糕。

相對基于Win32的定時器來說,?.NET?中一個很重要的改進就是創建不同的線程,該線程阻塞指定的時間,然后通知一個回調函數。這里的定時器不需要Microsoft的消息系統,所以這樣就更精確,而且還能用于控制臺應用程序中。以下代碼顯示了這個技術的一種實現:

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應用中。每個定時器創建獨立的線程,并且等待指定的時間,然后呼叫回調函數。猶如你看到的那樣,這個實現的成本會非常高。如果你的應用程序使用了多個定時器,相對的線程數量也會隨著使用定時器的數量而增長。

現在我們有.NET?提供的線程池,我們可以從池中改變請求的等待函數,這樣就十分有效,而且會提升系統的性能。我們會遇到兩個問題:

n??????????假如線程池已滿(所有的線程都在運行中),那么這個請求排到隊列中等待,而且定時器不在精確。

n??????????假如創建了多個定時器,線程池會因為等待它們時間片失效而非常忙。

為了避免這些問題,.NET框架的線程池提供了獨立于時間的請求。用了這個函數,我們可以不用任何線程就可以擁有成千上萬個定時器,一旦時間片失效,這時,線程池將會處理這些請求。

這些特色出現在兩個不同的類中:

?????????System.Threading.Timer

???????????????????定時器的簡單版本,它運行開發人員向線程池中的定期執行的程序指定一個代理(delegate).

System.Timers.Timer

System.Threading.Timer的組件版本,允許開發人員把它拖放到一個窗口表單(form)中,可以把一個事件作為執行的函數。

這非常有助于理解上述兩個類與另外一個稱為System.Windows.Forms.Timer.的類。這個類只是封裝了Win32中消息機制的計數器,如果你不準備開發多線程應用,那么就可以用這個類。

在下面的例子中,我們將用System.Threading.Timer?類,定時器的最簡單實現,我們只需要如下定義的構造方法

public Timer(TimerCallback callback,
?? object state,
?? int dueTime,
?? int period);

對于第一個參數(callback),我們可以指定定時執行的函數;第二個參數是傳遞給函數的通用對象;第三個參數是計時器開始執行前的延時;最后一個參數period,是兩個執行之間的毫秒數。

下面的例子創建了兩個定時器,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

猶如你看到的那樣,兩個定時器中的所有函數調用都在同一個線程中執行(ID = 2),應用程序使用的資源最小化了。

?

同步對象的執行

相對于定時器,.NET線程池允許在執行函數上同步對象,為了在多線程環境中的各線程之間共享資源,我們需要用.NET同步對象。

如果我們沒有線程,或者線程必須阻塞直到事件收到信號,就像我前面提到一樣,這會增加應用程序中總的線程數量,結果導致系統需要更多的資源和CPU時間。

線程池允許我們把請求進行排隊,直到某個特殊的同步對象收到信號后執行。如果這個信號沒有收到,請求函數將不需要任何線程,所以可以保證系統性能最優化。ThreadPool類提供了下面的方法:

public static RegisteredWaitHandle RegisterWaitForSingleObject(
?? WaitHandle waitObject,
?? WaitOrTimerCallback callBack,
?? object state,
?? int millisecondsTimeOutInterval,
?? bool executeOnlyOnce);

?

第一個參數,waitObject?可以是任何繼承于WaitHandle的對象:

???????? Mutex

???? ManualResetEvent

???? AutoResetEvent

就像你看到的那樣,只有系統的同步對象才能用在這里,就是繼承自WaitHandle的對象。你不能用其它任何的同步機制,比如moniter?或者?read-write?鎖。剩余的參數允許我們指明當一個對象收到信號后執行的函數(callBack;一個傳遞給函數的狀態(state);?線程池等待對象的最大時間?(millisecondsTimeOutInterval)?和一個標識表明對象收到信號時函數只能執行一次,?(executeOnlyOnce).?下面的代理聲明目的是用在函數的回調:

delegate void WaitOrTimerCallback(
?? object state,
?? bool timedOut);
?
如果參數 timeout 設置的最大時間已經失效,但是沒有同步對象收到信號的花,這個函數就會被調用。
下面的例子用了一個手工事件和一個互斥量來通知線程池中的執行函數:
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);
?? }
}

結束顯示兩個函數都在線程池的同一線程中執行:

1...2...3...4...5...
Synchronization object signaled, Thread: 6 Is pool: True
Synchronization object signaled, Thread: 6 Is pool: True

?

異步I/O操作

線程池常見的應用場景就是I/O操作。多數應用系統需要讀磁盤,數據發送到Sockets,因特網連接等等。所有的這些操作都有一些特征,直到他們執行操作時,才需要CPU時間。.NET?框架為所有這些可能執行的異步操作提供了I/O類。當這些操作執行完后,線程池中特定的函數會執行。尤其是在服務器應用程序中執行多線程異步操作,性能會更好。

在第一個例子中,我們將把一個文件異步寫到硬盤中。看一看FileStream?的構造方法是如何使用的:

public FileStream(
?? string path,
?? FileMode mode,
?? FleAccess access,
?? FleShare share,
?? int bufferSize,
?? bool useAsync);

最后一個參數非常有趣,我們應該對異步執行文件的操作設置useAsync為True。如果我們沒有這樣做,即使我們用了異步函數,它們的操作仍然會被主叫線程阻塞。

下面的例子說明了用一旦FileStream BeginWrite方法寫文件操作結束,線程池中的一個回調函數將會被執行。注意我們可以在任何時候訪問IAsyncResult接口,它可以用來了解當前操作的狀態。我們可以用CompletedSynchronously?屬性指示一個異步操作是否完成,而當一個操作結束時,IsCompleted?屬性會設上一個值。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);
?? }
}

輸出的結果顯示了操作是異步執行的,一旦操作結束后,用戶的函數就在線程池中執行。

Main thread:9
Synchronous operation: False
Operation finished: True on thread ID:10, is pool: True

在應用Sockets的場景中,由于I/O操作通常比磁盤操作慢,這時用線程池就顯得尤為重要。過程跟前面提到的差不多,Socket?類提供了多個方法用于執行異步操作:

???????? BeginRecieve

???????? BeginSend

???????? BeginConnect

???????? BeginAccept

假如你的服務器應用使用了Socket來與客戶端通訊,一定會用到這些方法。這種方法取代了對每個客戶端連接都啟用一個線程的做法,所有的操作都在線程池中異步執行。

?????????下面的例子用另外一個支持異步操作的類,HttpWebRequest用這個類,我們可以建立一個到Web服務器的連接。這個方法叫BeginGetResponse,?但在這個例子中有一個很重要的區別。在上面最后一個示例中,我們沒有用到從操作中返回的結果。但是,我們現在需要當一個操作結束時從Web服務器返回的響應,為了接收到這個信息,.NET中所有提供異步操作的類都提供了成對的方法。在HttpWebRequest這個類中,這個成對的方法就是:BeginGetResponseEndGetResponse用了End版本,我們可以接收操作的結果。在我們的示例中,EndGetResponse?會從Web服務器接收響應。

雖然可以在任何時間調用EndGetResponse?方法,但在我們的例子中是在回調函數中做的。僅僅是因為我們想知道已經做了異步請求。如果我們在之前調用EndGetResponse?,這個調用將一直阻塞到操作完成。

?????????在下面的例子中,我們發送一個請求到Microsoft Web,然后顯示了接收到響應的大小。

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);
?? }
}

下面剛開始結果信息表明,異步操作正在執行:

Synchronous: False

過了一會兒,響應接收到了。下面的結果顯示:

Response received on pool: True
?? Response size: 27331

就像你看到的那樣,一旦收到響應,線程池的異步函數就會執行。

?

監視線程池

ThreadPool 類提供了兩個方法用來查詢線程池的狀態。第一個是我們可以從線程池獲取當前可用的線程數量:
public static void GetAvailableThreads(
?? out int workerThreads,
?? out int completionPortThreads);

從方法中你可以看到兩種不同的線程:

?????????WorkerThreads

???????工作線程是標準系統池的一部分。它們是被.NET框架托管的標準線程,多數函數是在這里執行的。顯式的用戶請求(QueueUserWorkItem方法),基于異步對象的方法(RegisterWaitForSingleObject)和定時器(Timer類)

?

CompletionPortThreads

這種線程常常用來I/O操作,Windows NT, Windows 2000?和?Windows XP提供了一個步執行的對象,叫做IOCompletionPort把API和異步對象關聯起來,用少量的資源和有效的方法,我們就可以調用系統線程池的異步I/O操作。但是在Windows 95, Windows 98,?和?Windows Me有一些局限。比如:?在某些設備上,沒有提供IOCompletionPorts?功能和一些異步操作,如磁盤和郵件槽。在這里你可以看到.NET框架的最大特色:一次編譯,可以在多個系統下運行。根據不同的目標平臺,.NET?框架會決定是否使用IOCompletionPorts API,用最少的資源達到最好的性能。

這節包含一個使用Socket?類的例子。在這個示例中,我們將異步建立一個連接到本地的Web服務器,然后發送一個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?下運行這個程序,你將會看到如下結果:

Connected to localhost:80
WorkerThreads: 24, CompletionPortThreads: 25
Request sent to localhost:80
WorkerThreads: 25, CompletionPortThreads: 24

?

如你所看到地那樣,連接用了工作線程,而發送數據用了一個完成端口(CompletionPort),接著看下面的順序:

1.???我們得到一個本地IP地址,然后異步連接到那里。

2.???Socket在工作線程上執行異步連接操作,因為在Socket上,不能用Windows?的IOCompletionPorts來建立連接。

3.???一旦連接建立了,Socket類調用指明的函數ConnectCallback,這個回調函數顯示了線程池中可用的線程數量。我們可以看到這些是在工作線程中執行的。

4.???在用ASCII碼對Get請求進行編碼后,我們用BeginSend方法從同樣的函數ConnectCallback?中發送一個異步請求。

5.???Socket上的發送和接收操作可以通過IOCompletionPort?來執行異步操作,所以當請求做完后,回調函數就會在一個CompletionPort類型的線程中執行。因為函數本身顯示了可用的線程數量,所以我們可以通過這個來查看,對應的完成端口數已經減少了多少。

如果我們在Windows 95, Windows 98,?或者?Windows Me平臺上運行相同的代碼,會出現相同的連接結果,請求將被發送到工作線程,而非完成端口。你應該知道的很重要的一點就是,Socket類總是會利用最優的可用機制,所以你在開發應用時,可以不用考慮目標平臺是什么。

???????你已經看到在上面的例子中每種類型的線程可用的最大數是25。我們可以用GetMaxThreads返回這個值:

public static void GetMaxThreads(
?? out int workerThreads,
?? out int completionPortThreads);

一旦到了最大的數量,就不會創建新線程,所有的請求都將被排隊。假如你看過ThreadPool類的所有方法,你將發現沒有一個允許我們更改最大數的方法。就像我們前面提到的那樣,線程池是每個處理過程的唯一共享資源。這就是為什么不可能讓應用程序域去更改這個配置的原因。想象一下出現這種情況的后果,如果有第三方組件把線程池中線程的最大數改為1,整個應用都會停止工作,甚至在進程中其它的應用程序域都將受到影響。同樣的原因,公共語言運行時的宿主也有可能去更改這個配置。比如:ASP.NET允許系統管理員更改這個數字。

?

死鎖

在你的應用程序使用線程池之前,還有一個東西你應該知道:死鎖。在線程池中執行一個實現不好的異步對象可能導致你的整個應用系統中止運行。

???????設想你的代碼中有個方法,它需要通過Socket連接到一個Web服務器上。一個可能的實現就是用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);
?? }
}

多快,多好。調用BeginConnect使異步操作在線程池中執行,而EndConnect一直阻塞到連接被建立。

???????如果線程池中的一個執行函數中用了這個類的方法,將會發生什么事情呢?設想線程池的大小只有兩個線程,然后用我們的連接類創建了兩個異步對象。當這兩個函數同時在池中執行時,線程池已經沒有用于其它請求的空間了,除非直到某個函數結束。問題是這些函數調用了我們類中的Connect方法,這個方法在線程池中又發起了一個異步操作。但線程池一直是滿的,所以請求就一直等待任何空閑線程的出現。不幸的是,這將永遠不會發生,因為使用線程池的函數正等待隊列函數的結束。結論就是:我們的應用系統已經阻塞了。

???????我們以此推斷25個線程的線程池的行為。假如25個函數都等待異步對象操作的結束。結果將是一樣的,死鎖一樣會出現。

???????在下面的代碼片斷中,我們使用了這個類來說明問題:

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();
?? }
}

如果你運行這個例子,你將看到池中的線程是如何把線程的可用數量減少到零的,接著應用中止,死鎖出現了。

???????如果你想在你的應用中避免出現死鎖,永遠不要阻塞正在等待線程池中的其它函數的線程。這看起來很容易,但記住這個規則意味著有兩條:

n??????????不要創建這樣的類,它的同步方法在等待異步函數。因為這種類可能被線程池中的線程調用。

n??????????不要在任何異步函數中使用這樣的類,如果它正等待著這個異步函數。

如果你想檢測到應用中的死鎖情況,那么就當你的系統掛起時,檢查線程池中的線程可用數。線程的可用數量已經沒有并且CPU的使用率為0?,這是很明顯的死鎖癥狀。你應該檢查你的代碼,以確定哪個在線程中執行的函數正在等待異步操作,然后刪除它。

??????

有關安全性

?????????如果你再看看ThreadPool類,你會看到有兩個方法我們沒有用到,UnsafeQueueUserWorkItemUnsafeRegisterWaitForSingleObject。?為了完全理解這些方法,首先,我們必須回憶?.NET框架中安全策略是怎么運作的。

???????? ?Windows安全機制是關注資源。操作系統本身允許對文件,用戶,注冊表鍵值和任何其它的系統資源設定權限。這種方法對應用系統的用戶認證非常有效,但當出現用戶對他使用的系統產生不信任的情況時,這就會有些局限性。例如這些程序是從Internet下載的。在這種情況下,一旦用戶安裝了這個程序,它就可以執行用戶權限范圍內的任何操作。舉個例子,假如用戶可以刪除他公司內的任何共享文件,任何從Internet下載的程序也都可以這樣做。

???????? .NET?提供了應用到程序的安全性策略,而不是用戶。這就是說,在用戶權限的范圍內,我們可以限制任何執行單元(程序集)使用的資源。通過MMC,我們可以根據條件定義一組程序集,然后為每組設置不同的策略,一個典型的例子就是限制從Internet下載的程序訪問磁盤的權限。

?????????為了讓這個功能運轉起來,.NET?框架必須維護一個不同程序集之間的調用棧。假設一個應用沒有權限訪問磁盤,但是它調用了一個對整個系統都可以訪問的類庫,當第二個程序集執行一個磁盤的操作時,設置到這個程序集的權限允許這樣做,但是權限不會被應用到主叫程序集,.NET不僅要檢查當前程序集的權限,而且會檢查整個調用棧的權限。這個棧已經被高度優化了,但是它們給兩個不同程序集之間的調用增加了額外的負擔。

?????????UnsafeQueueUserWorkItem?,?UnsafeRegisterWaitForSingleObject與?QueueUserWorkItem?,RegisterWaitForSingleObject兩個方法類似。由于是非安全版本不會維護它們執行函數之間的調用棧,所以非安全版本運行的更快些。但是回調函數將只在當前程序集的安全策略下執行,它就不能應用權限到整個調用棧中的程序集。

?????????我的建議是僅在性能非常重要的、安全已經控制好的極端情況下才用非安全版本。例如,你構建的應用程序不會被其它的程序集調用,或者僅被很明確清楚的程序集使用,那么你可以用非安全版本。如果你開發的類庫會被第三方應用程序中使用,那么你就不應該用這些方法,因為它們可能用你的庫獲取訪問系統資源的權限。

?????????在下面例子中,你可以看到用UnsafeQueueUserWorkItem方法的風險。我們將構建兩個單獨的程序集,在第一個程序集中我們將在線程池中創建一個文件,然后我們將導出一個類以使這個操作可以被其它的程序集執行。

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?方法去創建一個文件:

using System;
namespace ThreadSecurityTest
{
?? class MainApp
?? {
????? static void Main()
????? {
???????? PoolCheck pc = new PoolCheck();
???????? pc.CheckIt();
???????? Console.ReadLine();
????? }
?? }
}

編譯這兩個程序集,然后運行main應用。默認情況下,你的應用被配置為允許執行磁盤操作,所以系統成功生成文件。

???? File created

現在,打開.NET框架的配置。為了簡化這個例子,我們僅創建一個代碼組關聯到main應用。接著展開?運行庫安全策略/?計算機/?代碼組/ All_Code /,增加一個叫ThreadSecurityTest的組。在向導中,選擇Hash?條件并導入Hash到我們的應用中,設置為Internet級別,并選擇“該策略級別將只具有與此代碼組關聯的權限集中的權限”選項。

運行應用程序,看看會發生什么情況:

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.

我們的策略開始工作,系統已經不能創建文件了。這是因為.NET框架為我們維護了一個調用棧才使它成為了可能,雖然創建文件的庫有權限去訪問系統。

現在把庫中的QueueUserWorkItem替換為UnsafeQueueUserWorkItem,再次編譯程序集,然后運行Main程序。現在的結果是:

File created

即使我們的系統沒有足夠的權限去訪問磁盤,但我們已經創建了一個向整個系統公開它的功能的庫,卻沒有維護它的調用棧。記住一個金牌規則:?僅在你的代碼不允許讓其它的應用系統調用,或者當你想要嚴格限制訪問很明確清楚的程序集,才使用非安全的函數。

?

結束

?????????在這篇文章中,我們知道了為什么在我們的服務器應用中需要使用線程池來優化資源和CPU的利用。我們學習了一個線程池是如何實現的,需要考慮多個因素如:CPU使用的百分比,隊列請求或者系統的處理器數量。

???????? .NET提供了豐富的線程池的功能以讓我們的應用程序使用,?并且與.NET框架的類緊密地集成在一起。這個線程池是高度優化了的,它只需要最少的CPU時間和資源,而且總能適應目標平臺。

?????????因為與框架集成在一起,所以框架中的大部分類都提供了使用線程池的內在功能,給開發人員提供了集中管理和監視應用中的線程池的功能。鼓勵第三方組件使用線程池,這樣它們的客戶就可以享受.NET所提供的全部功能。允許執行用戶函數,定時器,I/O操作和同步對象。

?????????假如你在開發服務器應用系統,只要有可能就在你的請求處理系統中使用線程池。或者你開發了一個讓服務器程序使用的庫,那么盡可能提供系統線程池的異步對象處理。

轉載于:https://www.cnblogs.com/gjhjoy/p/3547996.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/377313.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/377313.shtml
英文地址,請注明出處:http://en.pswp.cn/news/377313.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

折線分割平面

Input輸入數據的第一行是一個整數C,表示測試實例的個數&#xff0c;然后是C 行數據&#xff0c;每行包含一個整數n(0<n<10000),表示折線的數量。Output對于每個測試實例&#xff0c;請輸出平面的最大分割數&#xff0c;每個實例的輸出占一行。Sample Input2 1 2Sample Ou…

《c++特性》

目錄多態構造函數和析構函數存在多態嗎&#xff1f;虛函數表虛析構函數純虛函數和抽象類運行時多態和編譯時多態的區別繼承設計實例指針對象和普通對象的區別正確初始化派生類方式繼承和賦值的兼容規則protected 和 private 繼承基類與派生類的指針強制轉換如何用C實現C的三大特…

Scala中的while循環

在Scala中的while循環 (while loop in Scala) while loop in Scala is used to run a block of code multiple numbers of time. The number of executions is defined by an entry condition. If this condition is TRUE the code will run otherwise it will not run. Scala中…

Linux操作系統啟動過程

在做開發的過程中&#xff0c;突然發現&#xff0c;要對系統做一些有意義的改變&#xff0c;必須要對操作系統的啟動過程有一定的了解&#xff0c;不然就是修改你都不知道從哪里下手啊&#xff0c;然后就是找來資料看&#xff0c;去網上看別人的博客&#xff0c;有了前一周一些…

方法命名的區別

GetDecimalFromString ExtractDecimal 這2個方法名那個比較好呢。上邊的明顯的是中式英語&#xff0c;單詞拼湊而成的。下邊的更加流暢一些。方法名稱取名還是很有要求的。要通俗易懂還要符合文法。從上邊的可以擴展出什么想法呢。 ExtractDecimalExtractDoubleExtractInt16Ext…

工作排序問題

Problem statement: 問題陳述&#xff1a; Given an array of jobs where every job has a deadline and a profit. Profit can be earned only if the job is finished before the deadline. It is also given that every job takes a single unit of time, so the minimum p…

牛客網與leetcode刷題(高頻題中簡單or中等的)

目錄1、反轉鏈表2、排序3、先序中序后序遍歷4、最小的k個數5、子數組的最大累加和6、 用兩個棧實現隊列7、142. 環形鏈表 II8、20. 有效的括號9、最長公共子串(動態規劃),磕磕絆絆10、二叉樹之字形層序遍歷11、重建二叉樹12、LRU緩存13、合并兩個有序鏈表15、大數加法16、一個二…

AMUL的完整形式是什么?

AMUL&#xff1a;阿南德牛奶聯盟有限公司 (AMUL: Anand Milk Union Limited) AMUL is an abbreviation of Anand Milk Union Limited. It is an Indian milk product cooperative dairy organization that is based in the small town of Anand in the state of Gujarat. AMUL …

mochiweb 源碼閱讀(十一)

大家好&#xff0c;今天周六&#xff0c;繼續接著上一篇&#xff0c;跟大家分享mochiweb源碼。上一篇&#xff0c;最后我們看到了mochiweb_socket_server:listen/3函數&#xff1a; listen(Port, Opts, State#mochiweb_socket_server{sslSsl, ssl_optsSslOpts}) ->case moch…

Android下拉刷新完全解析,教你如何一分鐘實現下拉刷新功能 (轉)

轉載請注明出處&#xff1a;http://blog.csdn.net/guolin_blog/article/details/9255575 最 近項目中需要用到ListView下拉刷新的功能&#xff0c;一開始想圖省事&#xff0c;在網上直接找一個現成的&#xff0c;可是嘗試了網上多個版本的下拉刷新之后發現效果都不怎么理 想。有…

Python中的append()和extend()

列出append()方法 (List append() method) append() method is used to insert an element or a list object to the list and length of the List increased by the 1. append()方法用于將元素或列表對象插入列表&#xff0c;并且列表長度增加1。 Syntax: 句法&#xff1a; …

紅黑樹的實現

目錄1、紅黑樹原理1、紅黑樹性質2、變換規則&#xff08;從插入結點的角度來講&#xff09;1.變色2.左旋3.右旋3、刪除結點需要注意的地方2、代碼1、定義結點以及構造函數2、定義紅黑樹類以及聲明它的方法3、左旋4、右旋5、插入操作6、修正操作7、刪除操作3、參考鏈接1、紅黑樹…

118 - ZOJ Monthly, July 2012

http://acm.zju.edu.cn/onlinejudge/showContestProblems.do?contestId339 都是賽后做的。。。弱爆了 A題是找由2和5組成的數字的個數 直接打個表就行了 只是比賽的時候不知道怎么打表啊。。 View Code #include<cstdio> #include<cstring> #include<algorith…

edp1.2和edp1.4_EDP??的完整形式是什么?

edp1.2和edp1.4EDP??&#xff1a;電子數據處理 (EDP: Electronic Data Processing) EDP is an abbreviation of Electronic Data Processing. It alludes to the functioning of operations of commercial data, documents processing of storing, with the use of a compute…

高效讀書心得

1.盡量閱讀中文版 雖然有人英文很強&#xff0c;有的翻譯很差&#xff0c;但AnyWay 中文閱讀與理解的時間&#xff0c;略讀與快速定位感興趣內容的速度還是要快一些。 2.即時批注、總結筆記與交流 雖然愛書&#xff0c;但發現最有效的讀書方式還是不斷的制造脂批本&…

《MySQL——增刪改查以及常用語法》

目錄登錄和退出MySQL服務器基本語法&#xff08;增刪改查&#xff09;登錄和退出MySQL服務器 # 登錄MySQL 密碼 $ mysql -u root -p12345612 # 退出MySQL數據庫服務器 exit;基本語法&#xff08;增刪改查&#xff09; -- 顯示所有數據庫 show databases;-- 創建數據庫 CREA…

WCF簡介

一、簡介 WCF是Windows Communication Foundation縮寫&#xff0c;是Microsoft為構建面向服務的應用提供的分布式通信編程框架&#xff0c;是.NET Framework 3.5的重要組成部分。使用該框架&#xff0c;開發人員可以構建跨平臺、安全、可靠和支持事務處理的企業級互聯應用解決方…

css鏈接樣式_CSS中的樣式鏈接

css鏈接樣式CSS樣式鏈接 (CSS Styling Links) The links in CSS can be styled in various ways to make our website more presentable and attractive. The links can also be styled depending on their states e.g. visited, active, hover, etc. CSS中的鏈接可以通過各種方…

《MySQL——約束》

目錄主鍵約束唯一主鍵非空約束默認約束外鍵約束主鍵約束 -- 主鍵約束 -- 使某個字段不重復且不得為空&#xff0c;確保表內所有數據的唯一性。 CREATE TABLE user (id INT PRIMARY KEY,name VARCHAR(20) );-- 聯合主鍵 -- 聯合主鍵中的每個字段都不能為空&#xff0c;并且加起…

UIControl事件

CHENYILONG BlogUIControl事件 FullscreenUIControl事件1.UIControlEventTouchDown單點觸摸按下事件&#xff1a;用戶點觸屏幕&#xff0c;或者又有新手指落下的時候。2.UIControlEventTouchDownRepeat多點觸摸按下事件&#xff0c;點觸計數大于1&#xff1a;用戶按下第二、三、…