異步IO、APC、IO完成端口、線程池與高性能服務器之一 異步IO
背景:輪詢 PIO DMA 中斷
早期IO設備的速度與CPU相比,還不是太懸殊。CPU定時輪詢一遍IO設備,看看有無處理要求,有則加以處理,完成后返回繼續工作。至今,軟盤驅動器還保留著這種輪詢工作方式。
隨 著CPU性能的迅速提高,這種效率低下的工作方式浪費了大量的CPU時間。因此,中斷工作方式開始成為普遍采用的技術。這種技術使得IO設備在需要得到服 務時,能夠產生一個硬件中斷,迫使CPU放棄目前的處理任務,進入特定的中斷服務過程,中斷服務完成后,再繼續原先的處理。這樣一來,IO設備和CPU可 以同時進行處理,從而避免了CPU等待IO完成。
早期數據的傳輸方式主要是PIO(程控IO)方式。通過對IO地址編程方式的方式來傳輸 數據。比如串行口,軟件每次往串行口上寫一個字節數據,串口設備完成傳輸任務后,將會產生一個中斷,然后軟件再次重復直到全部數據發送完成。性能更好的硬 件設備提供一個FIFO(先進先出緩沖部件),可以讓軟件一次傳輸更多的字節。
顯然,使用PIO方式對于高速IO設備來說,還是占用了太 多的CPU時間(因為需要通過CPU編程控制傳輸)。而DMA(直接內存訪問)方式能夠極大地減少CPU處理時間。CPU僅需告訴DMA控制器數據塊的起 始地址和大小,以后DMA控制器就可以自行在內存和設備之間傳輸數據,其間CPU可以處理其他任務。數據傳輸完畢后將會產生一個中斷。
同步文件IO和異步文件IO
下面摘抄于MSDN《synchronous file I/O and asynchronous file I/O》。
有兩種類型的文件IO同步:同步文件IO和異步文件IO。異步文件IO也就是重疊IO。
在同步文件IO中,線程啟動一個IO操作然后就立即進入等待狀態,直到IO操作完成后才醒來繼續執行。而異步文件IO方式中,線程發送一個IO請求到內核,然后繼續處理其他的事情,內核完成IO請求后,將會通知線程IO操作完成了。
如果IO請求需要大量時間執行的話,異步文件IO方式可以顯著提高效率,因為在線程等待的這段時間內,CPU將會調度其他線程進行執行,如果沒 有其他線程需要執行的話,這段時間將會浪費掉(可能會調度操作系統的零頁線程)。如果IO請求操作很快,用異步IO方式反而還低效,還不如用同步IO方 式。
同步IO在同一時刻只允許一個IO操作,也就是說對于同一個文件句柄的IO操作是序列化的,即使使用兩個線程也不能同時對同一個文件句柄同時發出讀寫操作。重疊IO允許一個或多個線程同時發出IO請求。
異步IO在請求完成時,通過將文件句柄設為有信號狀態來通知應用程序,或者應用程序通過GetOverlappedResult察看IO請求是否完成,也可以通過一個事件對象來通知應用程序。
參考書目
1, MSDN Library
2, 《Windows高級編程指南》
3, 《Windows核心編程》
4, 《Windows 2000 設備驅動程序設計指南》
異步IO、APC、IO完成端口、線程池與高性能服務器之二 APC
Alertable IO(告警IO)提供了更有效的異步通知形式。ReadFileEx / WriteFileEx在發出IO請求的同時,提供一個回調函數(APC過程),當IO請求完成后,一旦線程進入可告警狀態,回調函數將會執行。
以下五個函數能夠使線程進入告警狀態:
SleepEx
WaitForSingleObjectEx
WaitForMultipleObjectsEx
SignalObjectAndWait
MsgWaitForMultipleObjectsEx
線 程進入告警狀態時,內核將會檢查線程的APC隊列,如果隊列中有APC,將會按FIFO方式依次執行。如果隊列為空,線程將會掛起等待事件對象。以后的某 個時刻,一旦APC進入隊列,線程將會被喚醒執行APC,同時等待函數返回WAIT_IO_COMPLETION。
QueueUserAPC可以用來人為投遞APC,只要目標線程處于告警狀態時,APC就能夠得到執行。
使用告警IO的主要缺點是發出IO請求的線程也必須是處理結果的線程,如果一個線程退出時還有未完成的IO請求,那么應用程序將永遠丟失IO完成通知。然而以后我們將會看到IO完成端口沒有這個限制。
下面的代碼演示了QueueUserAPC的用法。
/************************************************************************/
/* APC Test. */
/************************************************************************/
DWORD WINAPI WorkThread(PVOID pParam)
{
HANDLE Event = (HANDLE)pParam;
for(;;)
{
DWORD dwRet = WaitForSingleObjectEx(Event, INFINITE, TRUE);
if(dwRet == WAIT_OBJECT_0)
break;
else if(dwRet == WAIT_IO_COMPLETION)
printf("WAIT_IO_COMPLETION\n");
}
return 0;
}
VOID CALLBACK APCProc(DWORD dwParam)
{
printf("%s", (PVOID)dwParam);
}
void TestAPC(BOOL bFast)
{
HANDLE QuitEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
HANDLE hThread = CreateThread(NULL,
0,
WorkThread,
(PVOID)QuitEvent,
0,
NULL);
Sleep(100); // Wait for WorkThread initialized.
for(int i=5; i>0; i--)
{
QueueUserAPC(APCProc, hThread, (DWORD)(PVOID)"APC here\n");
if(!bFast)
Sleep(1000);
}
SetEvent(QuitEvent);
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
}
參考書目
1, MSDN Library
2, 《Windows高級編程指南》
3, 《Windows核心編程》
4, 《Windows 2000 設備驅動程序設計指南》
異步IO、APC、IO完成端口、線程池與高性能服務器之三 IO完成端口
IO完成端口
下面摘抄于MSDN《I/O Completion Ports》,smallfool翻譯,原文請參考CSDN文檔中心文章《I/O Completion Ports》, http://dev.csdn.net/Develop/article/29%5C29240.shtm 。
I/O 完成端口是一種機制,通過這個機制,應用程序在啟動時會首先創建一個線程池,然后該應用程序使用線程池處理異步I/O請求。這些線程被創建的唯一目的就是 用于處理I/O請求。對于處理大量并發異步I/O請求的應用程序來說,相比于在I/O請求發生時創建線程來說,使用完成端口(s)它就可以做的更快且更有 效率。
CreateIoCompletionPort函數會使一個I/O完成端口與一個或多個文件句柄發生關聯。當與一個完成端口相關的文件句柄 上啟動的異步I/O操作完成時,一個I/O完成包就會進入到該完成端口的隊列中。對于多個文件句柄來說,這種機制可以用來把多文件句柄的同步點放在單個對 象中。(言下之意,如果我們需要對每個句柄文件進行同步,一般而言我們需要多個對象(如:Event來同步),而我們使用IO Complete Port 來實現異步操作,我們可以同多個文件相關聯,每當一個文件中的異步操作完成,就會把一個complete package放到隊列中,這樣我們就可以使用這個來完成所有文件句柄的同步)
調用GetQueuedCompletionStatus函數,某 個線程就會等待一個完成包進入到完成端口的隊列中,而不是直接等待異步I/O請求完成。線程(們)就會阻塞于它們的運行在完成端口(按照后進先出隊列順序 的被釋放)。這就意味著當一個完成包進入到完成端口的隊列中時,系統會釋放最近被阻塞在該完成端口的線程。
調用GetQueuedCompletionStatus,線程就會將會與某個指定的完成端口建立聯系,一直延續其該線程的存在周期,或被指定了不同的完成端口,或者釋放了與完成端口的聯系。一個線程只能與最多不超過一個的完成端口發生聯系。
完 成端口最重要的特性就是并發量。完成端口的并發量可以在創建該完成端口時指定。該并發量限制了與該完成端口相關聯的可運行線程的數目。當與該完成端口相關 聯的可運行線程的總數目達到了該并發量,系統就會阻塞任何與該完成端口相關聯的后續線程的執行,直到與該完成端口相關聯的可運行線程數目下降到小于該并發 量為止。最有效的假想是發生在有完成包在隊列中等待,而沒有等待被滿足,因為此時完成端口達到了其并發量的極限。此時,一個正在運行中的線程調用 GetQueuedCompletionStatus時,它就會立刻從隊列中取走該完成包。這樣就不存在著環境的切換,因為該處于運行中的線程就會連續不 斷地從隊列中取走完成包,而其他的線程就不能運行了。
對于并發量最好的挑選值就是您計算機中CPU的數目。如果您的事務處理需要一個漫長的計算時間,一個比較大的并發量可以允許更多線程來運行。雖然完成每個事務處理需要花費更長的時間,但更多的事務可以同時被處理。對于應用程序來說,很容易通過測試并發量來獲得最好的效果。
PostQueuedCompletionStatus函數允許應用程序可以針對自定義的專用I/O完成包進行排隊,而無需啟動一個異步I/O操作。這點對于通知外部事件的工作者線程來說很有用。
在沒有更多的引用針對某個完成端口時,需要釋放該完成端口。該完成端口句柄以及與該完成端口相關聯的所有文件句柄都需要被釋放。調用CloseHandle可以釋放完成端口的句柄。
下面的代碼利用IO完成端口做了一個簡單的線程池。
/************************************************************************/
/* Test IOCompletePort. */
/************************************************************************/
DWORD WINAPI IOCPWorkThread(PVOID pParam)
{
HANDLE CompletePort = (HANDLE)pParam;
PVOID UserParam;
WORK_ITEM_PROC UserProc;
LPOVERLAPPED pOverlapped;
for(;;)
{
BOOL bRet = GetQueuedCompletionStatus(
CompletePort,
(LPDWORD)&UserParam,
(LPDWORD)&UserProc,
&pOverlapped,
INFINITE);
_ASSERT(bRet);
if(UserProc == NULL) // Quit signal.
break;
// execute user's proc.
UserProc(UserParam);
}
return 0;
}
void TestIOCompletePort(BOOL bWaitMode, LONG ThreadNum)
{
HANDLE CompletePort;
OVERLAPPED Overlapped = {0, 0, 0, 0, NULL};
CompletePort = CreateIoCompletionPort(
INVALID_HANDLE_VALUE,
NULL,
NULL,
0);
// Create threads.
for(int i=0; i<ThreadNum; i++)
{
HANDLE hThread = CreateThread(NULL,
0,
IOCPWorkThread,
CompletePort,
0,
NULL);
CloseHandle(hThread);
}
CompleteEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
BeginTime = GetTickCount();
ItemCount = 20;
for(i=0; i<20; i++)
{
PostQueuedCompletionStatus(
CompletePort,
(DWORD)bWaitMode,
(DWORD)UserProc1,
&Overlapped);
}
WaitForSingleObject(CompleteEvent, INFINITE);
CloseHandle(CompleteEvent);
// Destroy all threads.
for(i=0; i<ThreadNum; i++)
{
PostQueuedCompletionStatus(
CompletePort,
NULL,
NULL,
&Overlapped);
}
Sleep(1000); // wait all thread exit.
CloseHandle(CompletePort);
}
參考書目
1, MSDN Library
2, 《Windows高級編程指南》
3, 《Windows核心編程》
4, 《Windows 2000 設備驅動程序設計指南》
異步IO、APC、IO完成端口、線程池與高性能服務器之四 線程池
線程池
下面摘抄于MSDN《Thread Pooling》。
有 許多應用程序創建的線程花費了大量時間在睡眠狀態來等待事件的發生。還有一些線程進入睡眠狀態后定期被喚醒以輪詢工作方式來改變或者更新狀態信息。線程池 可以讓你更有效地使用線程,它為你的應用程序提供一個由系統管理的工作者線程池。至少會有一個線程來監聽放到線程池的所有等待操作,當等待操作完成后,線 程池中將會有一個工作者線程來執行相應的回調函數。
你也可以把沒有等待操作的工作項目放到線程池中,用QueueUserWorkItem函數來完成這個工作,把要執行的工作項目函數通過一個參數傳遞給線程池。工作項目被放到線程池中后,就不能再取消了。
Timer-queue timers和Registered wait operations也使用線程池來實現。他們的回調函數也放在線程池中。你也可以用BindIOCompletionCallback函數來投遞一個異 步IO操作,在IO完成端口上,回調函數也是由線程池線程來執行。
當第一次調用QueueUserWorkItem函數或者 BindIOCompletionCallback函數的時候,線程池被自動創建,或者Timer-queue timers或者Registered wait operations放入回調函數的時候,線程池也可以被創建。線程池可以創建的線程數量不限,僅受限于可用的內存,每一個線程使用默認的初始堆棧大小, 運行在默認的優先級上。
線程池中有兩種類型的線程:IO線程和非IO線程。IO線程等待在可告警狀態,工作項目作為APC放到IO線程中。如果你的工作項目需要線程執行在可警告狀態,你應該將它放到IO線程。
非IO工作者線程等待在IO完成端口上,使用非IO線程比IO線程效率更高,也就是說,只要有可能的話,盡量使用非IO線程。IO線程和非IO線程在異步IO操作沒有完成之前都不會退出。然而,不要在非IO線程中發出需要很長時間才能完成的異步IO請求。
正 確使用線程池的方法是,工作項目函數以及它將會調用到的所有函數都必須是線程池安全的。安全的函數不應該假設線程是一次性線程的或者是永久線程。一般來 說,應該避免使用線程本地存儲和發出需要永久線程的異步IO調用,比如說RegNotifyChangeKeyValue函數。如果需要在永久線程中執行 這樣的函數的話,可以給QueueUserWorkItem傳遞一個選項WT_EXECUTEINPERSISTENTTHREAD。
注意,線程池不能兼容COM的單線程套間(STA)模型。
為了更深入地講解操作系統實現的線程池的優越性,我們首先嘗試著自己實現一個簡單的線程池模型。
代碼如下:
/************************************************************************/
/* Test Our own thread pool. */
/************************************************************************/
typedef struct _THREAD_POOL
{
HANDLE QuitEvent;
HANDLE WorkItemSemaphore;
LONG WorkItemCount;
LIST_ENTRY WorkItemHeader;
CRITICAL_SECTION WorkItemLock;
LONG ThreadNum;
HANDLE *ThreadsArray;
}THREAD_POOL, *PTHREAD_POOL;
typedef VOID (*WORK_ITEM_PROC)(PVOID Param);
typedef struct _WORK_ITEM
{
LIST_ENTRY List;
WORK_ITEM_PROC UserProc;
PVOID UserParam;
}WORK_ITEM, *PWORK_ITEM;
DWORD WINAPI WorkerThread(PVOID pParam)
{
PTHREAD_POOL pThreadPool = (PTHREAD_POOL)pParam;
HANDLE Events[2];
Events[0] = pThreadPool->QuitEvent;
Events[1] = pThreadPool->WorkItemSemaphore;
for(;;)
{
DWORD dwRet = WaitForMultipleObjects(2, Events, FALSE, INFINITE);
if(dwRet == WAIT_OBJECT_0)
break;
//
// execute user's proc.
//
else if(dwRet == WAIT_OBJECT_0 +1)
{
PWORK_ITEM pWorkItem;
PLIST_ENTRY pList;
EnterCriticalSection(&pThreadPool->WorkItemLock);
_ASSERT(!IsListEmpty(&pThreadPool->WorkItemHeader));
pList = RemoveHeadList(&pThreadPool->WorkItemHeader);
LeaveCriticalSection(&pThreadPool->WorkItemLock);
pWorkItem = CONTAINING_RECORD(pList, WORK_ITEM, List);
pWorkItem->UserProc(pWorkItem->UserParam);
InterlockedDecrement(&pThreadPool->WorkItemCount);
free(pWorkItem);
}
else
{
_ASSERT(0);
break;
}
}
return 0;
}
BOOL InitializeThreadPool(PTHREAD_POOL pThreadPool, LONG ThreadNum)
{
pThreadPool->QuitEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
pThreadPool->WorkItemSemaphore = CreateSemaphore(NULL, 0, 0x7FFFFFFF, NULL);
pThreadPool->WorkItemCount = 0;
InitializeListHead(&pThreadPool->WorkItemHeader);
InitializeCriticalSection(&pThreadPool->WorkItemLock);
pThreadPool->ThreadNum = ThreadNum;
pThreadPool->ThreadsArray = (HANDLE*)malloc(sizeof(HANDLE) * ThreadNum);
for(int i=0; i<ThreadNum; i++)
{
pThreadPool->ThreadsArray[i] = CreateThread(NULL, 0, WorkerThread, pThreadPool, 0, NULL);
}
return TRUE;
}
VOID DestroyThreadPool(PTHREAD_POOL pThreadPool)
{
SetEvent(pThreadPool->QuitEvent);
for(int i=0; i<pThreadPool->ThreadNum; i++)
{
WaitForSingleObject(pThreadPool->ThreadsArray[i], INFINITE);
CloseHandle(pThreadPool->ThreadsArray[i]);
}
free(pThreadPool->ThreadsArray);
CloseHandle(pThreadPool->QuitEvent);
CloseHandle(pThreadPool->WorkItemSemaphore);
DeleteCriticalSection(&pThreadPool->WorkItemLock);
while(!IsListEmpty(&pThreadPool->WorkItemHeader))
{
PWORK_ITEM pWorkItem;
PLIST_ENTRY pList;
pList = RemoveHeadList(&pThreadPool->WorkItemHeader);
pWorkItem = CONTAINING_RECORD(pList, WORK_ITEM, List);
free(pWorkItem);
}
}
BOOL PostWorkItem(PTHREAD_POOL pThreadPool, WORK_ITEM_PROC UserProc, PVOID UserParam)
{
PWORK_ITEM pWorkItem = (PWORK_ITEM)malloc(sizeof(WORK_ITEM));
if(pWorkItem == NULL)
return FALSE;
pWorkItem->UserProc = UserProc;
pWorkItem->UserParam = UserParam;
EnterCriticalSection(&pThreadPool->WorkItemLock);
InsertTailList(&pThreadPool->WorkItemHeader, &pWorkItem->List);
LeaveCriticalSection(&pThreadPool->WorkItemLock);
InterlockedIncrement(&pThreadPool->WorkItemCount);
ReleaseSemaphore(pThreadPool->WorkItemSemaphore, 1, NULL);
return TRUE;
}
VOID UserProc1(PVOID dwParam)
{
WorkItem(dwParam);
}
void TestSimpleThreadPool(BOOL bWaitMode, LONG ThreadNum)
{
THREAD_POOL ThreadPool;
InitializeThreadPool(&ThreadPool, ThreadNum);
CompleteEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
BeginTime = GetTickCount();
ItemCount = 20;
for(int i=0; i<20; i++)
{
PostWorkItem(&ThreadPool, UserProc1, (PVOID)bWaitMode);
}
WaitForSingleObject(CompleteEvent, INFINITE);
CloseHandle(CompleteEvent);
DestroyThreadPool(&ThreadPool);
}
我們把工作項目放到一個隊列中,用一個信號量通知線程池,線程池中任意一個線程取出工作項目來執行,執行完畢之后,線程返回線程池,繼續等待新的工作項目。
線程池中線程的數量是固定的,預先創建好的,永久的線程,直到銷毀線程池的時候,這些線程才會被銷毀。
線程池中線程獲得工作項目的機會是均等的,隨機的,并沒有特別的方式保證哪一個線程具有特殊的優先獲得工作項目的機會。
而且,同一時刻可以并發運行的線程數目沒有任何限定。事實上,在我們的執行計算任務的演示代碼中,所有的線程都并發執行。
下面,我們再來看一下,完成同樣的任務,系統提供的線程池是如何運作的。
/************************************************************************/
/* QueueWorkItem Test. */
/************************************************************************/
DWORD BeginTime;
LONG ItemCount;
HANDLE CompleteEvent;
int compute()
{
srand(BeginTime);
for(int i=0; i<20 *1000 * 1000; i++)
rand();
return rand();
}
DWORD WINAPI WorkItem(LPVOID lpParameter)
{
BOOL bWaitMode = (BOOL)lpParameter;
if(bWaitMode)
Sleep(1000);
else
compute();
if(InterlockedDecrement(&ItemCount) == 0)
{
printf("Time total %d second.\n", GetTickCount() - BeginTime);
SetEvent(CompleteEvent);
}
return 0;
}
void TestWorkItem(BOOL bWaitMode, DWORD Flag)
{
CompleteEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
BeginTime = GetTickCount();
ItemCount = 20;
for(int i=0; i<20; i++)
{
QueueUserWorkItem(WorkItem, (PVOID)bWaitMode, Flag);
}
WaitForSingleObject(CompleteEvent, INFINITE);
CloseHandle(CompleteEvent);
}
很簡單,是吧?我們僅需要關注于我們的回調函數即可。但是與我們的簡單模擬來比,系統提供的線程池有著更多的優點。
首先,線程池中線程的數目是動態調整的,其次,線程池利用IO完成端口的特性,它可以限制并發運行的線程數目,默認情況下,將會限制為CPU的數目,這可以減少線程切換。它挑選最近執行過的線程再次投入執行,從而避免了不必要的線程切換。
系統提供的線程池背后的策略,我們下一節繼續再談。
參考書目
1, MSDN Library
2, 《Windows高級編程指南》
3, 《Windows核心編程》
4, 《Windows 2000 設備驅動程序設計指南》
正文
異步IO、APC、IO完成端口、線程池與高性能服務器之五 服務器的性能指標與實現高性能的途徑
服務器的性能指標
作為一個網絡服務器程序,性能永遠是第一位的指標。性能可以這樣定義:在給定的硬件條件和時間里,能夠處理的任務量。能夠最大限度地利用硬件性能的服務器設計才是良好的設計。
設計良好的服務器還應該考慮平均服務,對于每一個客戶端,服務器應該給予每個客戶端平均的服務,不能讓某一個客戶端長時間得不到服務而發生“饑餓”的狀況。
可伸縮性,也就是說,隨著硬件能力的提高,服務器的性能能夠隨之呈線性增長。
實現高性能的途徑
一 個實際的服務器的計算是很復雜的,往往是混合了IO計算和CPU計算。IO計算指計算任務中以IO為主的計算模型,比如文件服務器、郵件服務器等,混合了 大量的網絡IO和文件IO;CPU計算指計算任務中沒有或很少有IO,比如加密/解密,編碼/解碼,數學計算等等。
在CPU計算中,單線 程和多線程模型效果是相當的。《Win32多線程的性能》中說“在一個單處理器的計算機中,基于 CPU 的任務的并發執行速度不可能比串行執行速度快,但是我們可以看到,在 Windows NT 下線程創建和切換的額外開銷非常?。粚τ诜浅6痰挠嬎?,并發執行僅僅比串行執行慢 10%,而隨著計算長度的增加,這兩個時間就非常接近了。”
可見,對于純粹的CPU計算來說,如果只有一個CPU,多線程模型是不合適的??紤]一個執行密集的CPU計算的服務,如果有幾十個這樣的線程并發執行,過于頻繁地任務切換導致了不必要的性能損失。
在 編程實現上,單線程模型計算模型對于服務器程序設計是很不方便的。因此,對于CPU計算采用線程池工作模型是比較恰當的。 QueueUserWorkItem函數非常適合于將一個CPU計算放入線程池。線程池實現將會努力減少這種不必要的線程切換,而且控制并發線程的數目為 CPU的數目。
我們真正需要關心的是IO計算,一般的網絡服務器程序往往伴隨著大量的IO計算。提高性能的途徑在于要避免等待IO 的結束,造成CPU空閑,要盡量利用硬件能力,讓一個或多個IO設備與CPU并發執行。前面介紹的異步IO,APC,IO完成端口都可以達到這個目的。
對 于網絡服務器來說,如果客戶端并發請求數目比較少的話,用簡單的多線程模型就可以應付了。如果一個線程因為等待IO操作完成而被掛起,操作系統將會調度另 外一個就緒的線程投入運行,從而形成并發執行。經典的網絡服務器邏輯大多采用多線程/多進程方式,在一個客戶端發起到服務器的連接時,服務器將會創建一個 線程,讓這個新的線程來處理后續事務。這種以一個專門的線程/進程來代表一個客戶端對象的編程方法非常直觀,易于理解。
對于大型網絡服務 器程序來說,這種方式存在著局限性。首先,創建線程/進程和銷毀線程/進程的代價非常高昂,尤其是在服務器采用TCP“短連接”方式或UDP方式通訊的情 況下,例如,HTTP協議中,客戶端發起一個連接后,發送一個請求,服務器回應了這個請求后,連接也就被關閉了。如果采用經典方式設計HTTP服務器,那 么過于頻繁地創建線程/銷毀線程對性能造成的影響是很惡劣的。
其次,即使一個協議中采取TCP“長連接”,客戶端連上服務器后就一直保持 此連接,經典的設計方式也是有弊病的。如果客戶端并發請求量很高,同一時刻有很多客戶端等待服務器響應的情況下,將會有過多的線程并發執行,頻繁的線程切 換將用掉一部分計算能力。實際上,如果并發線程數目過多的話,往往會過早地耗盡物理內存,絕大部分時間耗費在線程切換上,因為線程切換的同時也將引起內存 調頁。最終導致服務器性能急劇下降,
對于一個需要應付同時有大量客戶端并發請求的網絡服務器來說,線程池是唯一的解決方案。線程池不光能夠避免頻繁地創建線程和銷毀線程,而且能夠用數目很少的線程就可以處理大量客戶端并發請求。
值得注意的是,對于一個壓力不大的網絡服務器程序設計,我們并不推薦以上任何技巧。在簡單的設計就能夠完成任務的情況下,把事情弄得很復雜是很不明智,很愚蠢的行為。
from:
http://blog.chinaunix.net/u2/67780/showart_2057137.html
原文地址 http://blog.chinaunix.net/u/14774/showart_88161.html