異步IO、APC、IO完成端口、線程池與高性能服務(wù)器之一 異步IO
背景:輪詢 PIO DMA 中斷
早期IO設(shè)備的速度與CPU相比,還不是太懸殊。CPU定時(shí)輪詢一遍IO設(shè)備,看看有無處理要求,有則加以處理,完成后返回繼續(xù)工作。至今,軟盤驅(qū)動(dòng)器還保留著這種輪詢工作方式。
隨 著CPU性能的迅速提高,這種效率低下的工作方式浪費(fèi)了大量的CPU時(shí)間。因此,中斷工作方式開始成為普遍采用的技術(shù)。這種技術(shù)使得IO設(shè)備在需要得到服 務(wù)時(shí),能夠產(chǎn)生一個(gè)硬件中斷,迫使CPU放棄目前的處理任務(wù),進(jìn)入特定的中斷服務(wù)過程,中斷服務(wù)完成后,再繼續(xù)原先的處理。這樣一來,IO設(shè)備和CPU可 以同時(shí)進(jìn)行處理,從而避免了CPU等待IO完成。
早期數(shù)據(jù)的傳輸方式主要是PIO(程控IO)方式。通過對IO地址編程方式的方式來傳輸 數(shù)據(jù)。比如串行口,軟件每次往串行口上寫一個(gè)字節(jié)數(shù)據(jù),串口設(shè)備完成傳輸任務(wù)后,將會(huì)產(chǎn)生一個(gè)中斷,然后軟件再次重復(fù)直到全部數(shù)據(jù)發(fā)送完成。性能更好的硬 件設(shè)備提供一個(gè)FIFO(先進(jìn)先出緩沖部件),可以讓軟件一次傳輸更多的字節(jié)。
顯然,使用PIO方式對于高速IO設(shè)備來說,還是占用了太 多的CPU時(shí)間(因?yàn)樾枰ㄟ^CPU編程控制傳輸)。而DMA(直接內(nèi)存訪問)方式能夠極大地減少CPU處理時(shí)間。CPU僅需告訴DMA控制器數(shù)據(jù)塊的起 始地址和大小,以后DMA控制器就可以自行在內(nèi)存和設(shè)備之間傳輸數(shù)據(jù),其間CPU可以處理其他任務(wù)。數(shù)據(jù)傳輸完畢后將會(huì)產(chǎn)生一個(gè)中斷。
同步文件IO和異步文件IO
下面摘抄于MSDN《synchronous file I/O and asynchronous file I/O》。
有兩種類型的文件IO同步:同步文件IO和異步文件IO。異步文件IO也就是重疊IO。
在同步文件IO中,線程啟動(dòng)一個(gè)IO操作然后就立即進(jìn)入等待狀態(tài),直到IO操作完成后才醒來繼續(xù)執(zhí)行。而異步文件IO方式中,線程發(fā)送一個(gè)IO請求到內(nèi)核,然后繼續(xù)處理其他的事情,內(nèi)核完成IO請求后,將會(huì)通知線程IO操作完成了。
如果IO請求需要大量時(shí)間執(zhí)行的話,異步文件IO方式可以顯著提高效率,因?yàn)樵诰€程等待的這段時(shí)間內(nèi),CPU將會(huì)調(diào)度其他線程進(jìn)行執(zhí)行,如果沒 有其他線程需要執(zhí)行的話,這段時(shí)間將會(huì)浪費(fèi)掉(可能會(huì)調(diào)度操作系統(tǒng)的零頁線程)。如果IO請求操作很快,用異步IO方式反而還低效,還不如用同步IO方 式。
同步IO在同一時(shí)刻只允許一個(gè)IO操作,也就是說對于同一個(gè)文件句柄的IO操作是序列化的,即使使用兩個(gè)線程也不能同時(shí)對同一個(gè)文件句柄同時(shí)發(fā)出讀寫操作。重疊IO允許一個(gè)或多個(gè)線程同時(shí)發(fā)出IO請求。
異步IO在請求完成時(shí),通過將文件句柄設(shè)為有信號狀態(tài)來通知應(yīng)用程序,或者應(yīng)用程序通過GetOverlappedResult察看IO請求是否完成,也可以通過一個(gè)事件對象來通知應(yīng)用程序。
參考書目
1, MSDN Library
2, 《Windows高級編程指南》
3, 《Windows核心編程》
4, 《Windows 2000 設(shè)備驅(qū)動(dòng)程序設(shè)計(jì)指南》
異步IO、APC、IO完成端口、線程池與高性能服務(wù)器之二 APC
Alertable IO(告警IO)提供了更有效的異步通知形式。ReadFileEx / WriteFileEx在發(fā)出IO請求的同時(shí),提供一個(gè)回調(diào)函數(shù)(APC過程),當(dāng)IO請求完成后,一旦線程進(jìn)入可告警狀態(tài),回調(diào)函數(shù)將會(huì)執(zhí)行。
以下五個(gè)函數(shù)能夠使線程進(jìn)入告警狀態(tài):
SleepEx
WaitForSingleObjectEx
WaitForMultipleObjectsEx
SignalObjectAndWait
MsgWaitForMultipleObjectsEx
線 程進(jìn)入告警狀態(tài)時(shí),內(nèi)核將會(huì)檢查線程的APC隊(duì)列,如果隊(duì)列中有APC,將會(huì)按FIFO方式依次執(zhí)行。如果隊(duì)列為空,線程將會(huì)掛起等待事件對象。以后的某 個(gè)時(shí)刻,一旦APC進(jìn)入隊(duì)列,線程將會(huì)被喚醒執(zhí)行APC,同時(shí)等待函數(shù)返回WAIT_IO_COMPLETION。
QueueUserAPC可以用來人為投遞APC,只要目標(biāo)線程處于告警狀態(tài)時(shí),APC就能夠得到執(zhí)行。
使用告警IO的主要缺點(diǎn)是發(fā)出IO請求的線程也必須是處理結(jié)果的線程,如果一個(gè)線程退出時(shí)還有未完成的IO請求,那么應(yīng)用程序?qū)⒂肋h(yuǎn)丟失IO完成通知。然而以后我們將會(huì)看到IO完成端口沒有這個(gè)限制。
下面的代碼演示了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 設(shè)備驅(qū)動(dòng)程序設(shè)計(jì)指南》
異步IO、APC、IO完成端口、線程池與高性能服務(wù)器之三 IO完成端口
IO完成端口
下面摘抄于MSDN《I/O Completion Ports》,smallfool翻譯,原文請參考CSDN文檔中心文章《I/O Completion Ports》, http://dev.csdn.net/Develop/article/29%5C29240.shtm 。
I/O 完成端口是一種機(jī)制,通過這個(gè)機(jī)制,應(yīng)用程序在啟動(dòng)時(shí)會(huì)首先創(chuàng)建一個(gè)線程池,然后該應(yīng)用程序使用線程池處理異步I/O請求。這些線程被創(chuàng)建的唯一目的就是 用于處理I/O請求。對于處理大量并發(fā)異步I/O請求的應(yīng)用程序來說,相比于在I/O請求發(fā)生時(shí)創(chuàng)建線程來說,使用完成端口(s)它就可以做的更快且更有 效率。
CreateIoCompletionPort函數(shù)會(huì)使一個(gè)I/O完成端口與一個(gè)或多個(gè)文件句柄發(fā)生關(guān)聯(lián)。當(dāng)與一個(gè)完成端口相關(guān)的文件句柄 上啟動(dòng)的異步I/O操作完成時(shí),一個(gè)I/O完成包就會(huì)進(jìn)入到該完成端口的隊(duì)列中。對于多個(gè)文件句柄來說,這種機(jī)制可以用來把多文件句柄的同步點(diǎn)放在單個(gè)對 象中。(言下之意,如果我們需要對每個(gè)句柄文件進(jìn)行同步,一般而言我們需要多個(gè)對象(如:Event來同步),而我們使用IO Complete Port 來實(shí)現(xiàn)異步操作,我們可以同多個(gè)文件相關(guān)聯(lián),每當(dāng)一個(gè)文件中的異步操作完成,就會(huì)把一個(gè)complete package放到隊(duì)列中,這樣我們就可以使用這個(gè)來完成所有文件句柄的同步)
調(diào)用GetQueuedCompletionStatus函數(shù),某 個(gè)線程就會(huì)等待一個(gè)完成包進(jìn)入到完成端口的隊(duì)列中,而不是直接等待異步I/O請求完成。線程(們)就會(huì)阻塞于它們的運(yùn)行在完成端口(按照后進(jìn)先出隊(duì)列順序 的被釋放)。這就意味著當(dāng)一個(gè)完成包進(jìn)入到完成端口的隊(duì)列中時(shí),系統(tǒng)會(huì)釋放最近被阻塞在該完成端口的線程。
調(diào)用GetQueuedCompletionStatus,線程就會(huì)將會(huì)與某個(gè)指定的完成端口建立聯(lián)系,一直延續(xù)其該線程的存在周期,或被指定了不同的完成端口,或者釋放了與完成端口的聯(lián)系。一個(gè)線程只能與最多不超過一個(gè)的完成端口發(fā)生聯(lián)系。
完 成端口最重要的特性就是并發(fā)量。完成端口的并發(fā)量可以在創(chuàng)建該完成端口時(shí)指定。該并發(fā)量限制了與該完成端口相關(guān)聯(lián)的可運(yùn)行線程的數(shù)目。當(dāng)與該完成端口相關(guān) 聯(lián)的可運(yùn)行線程的總數(shù)目達(dá)到了該并發(fā)量,系統(tǒng)就會(huì)阻塞任何與該完成端口相關(guān)聯(lián)的后續(xù)線程的執(zhí)行,直到與該完成端口相關(guān)聯(lián)的可運(yùn)行線程數(shù)目下降到小于該并發(fā) 量為止。最有效的假想是發(fā)生在有完成包在隊(duì)列中等待,而沒有等待被滿足,因?yàn)榇藭r(shí)完成端口達(dá)到了其并發(fā)量的極限。此時(shí),一個(gè)正在運(yùn)行中的線程調(diào)用 GetQueuedCompletionStatus時(shí),它就會(huì)立刻從隊(duì)列中取走該完成包。這樣就不存在著環(huán)境的切換,因?yàn)樵撎幱谶\(yùn)行中的線程就會(huì)連續(xù)不 斷地從隊(duì)列中取走完成包,而其他的線程就不能運(yùn)行了。
對于并發(fā)量最好的挑選值就是您計(jì)算機(jī)中CPU的數(shù)目。如果您的事務(wù)處理需要一個(gè)漫長的計(jì)算時(shí)間,一個(gè)比較大的并發(fā)量可以允許更多線程來運(yùn)行。雖然完成每個(gè)事務(wù)處理需要花費(fèi)更長的時(shí)間,但更多的事務(wù)可以同時(shí)被處理。對于應(yīng)用程序來說,很容易通過測試并發(fā)量來獲得最好的效果。
PostQueuedCompletionStatus函數(shù)允許應(yīng)用程序可以針對自定義的專用I/O完成包進(jìn)行排隊(duì),而無需啟動(dòng)一個(gè)異步I/O操作。這點(diǎn)對于通知外部事件的工作者線程來說很有用。
在沒有更多的引用針對某個(gè)完成端口時(shí),需要釋放該完成端口。該完成端口句柄以及與該完成端口相關(guān)聯(lián)的所有文件句柄都需要被釋放。調(diào)用CloseHandle可以釋放完成端口的句柄。
下面的代碼利用IO完成端口做了一個(gè)簡單的線程池。
/************************************************************************/
/* 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 設(shè)備驅(qū)動(dòng)程序設(shè)計(jì)指南》
異步IO、APC、IO完成端口、線程池與高性能服務(wù)器之四 線程池
線程池
下面摘抄于MSDN《Thread Pooling》。
有 許多應(yīng)用程序創(chuàng)建的線程花費(fèi)了大量時(shí)間在睡眠狀態(tài)來等待事件的發(fā)生。還有一些線程進(jìn)入睡眠狀態(tài)后定期被喚醒以輪詢工作方式來改變或者更新狀態(tài)信息。線程池 可以讓你更有效地使用線程,它為你的應(yīng)用程序提供一個(gè)由系統(tǒng)管理的工作者線程池。至少會(huì)有一個(gè)線程來監(jiān)聽放到線程池的所有等待操作,當(dāng)?shù)却僮魍瓿珊螅€ 程池中將會(huì)有一個(gè)工作者線程來執(zhí)行相應(yīng)的回調(diào)函數(shù)。
你也可以把沒有等待操作的工作項(xiàng)目放到線程池中,用QueueUserWorkItem函數(shù)來完成這個(gè)工作,把要執(zhí)行的工作項(xiàng)目函數(shù)通過一個(gè)參數(shù)傳遞給線程池。工作項(xiàng)目被放到線程池中后,就不能再取消了。
Timer-queue timers和Registered wait operations也使用線程池來實(shí)現(xiàn)。他們的回調(diào)函數(shù)也放在線程池中。你也可以用BindIOCompletionCallback函數(shù)來投遞一個(gè)異 步IO操作,在IO完成端口上,回調(diào)函數(shù)也是由線程池線程來執(zhí)行。
當(dāng)?shù)谝淮握{(diào)用QueueUserWorkItem函數(shù)或者 BindIOCompletionCallback函數(shù)的時(shí)候,線程池被自動(dòng)創(chuàng)建,或者Timer-queue timers或者Registered wait operations放入回調(diào)函數(shù)的時(shí)候,線程池也可以被創(chuàng)建。線程池可以創(chuàng)建的線程數(shù)量不限,僅受限于可用的內(nèi)存,每一個(gè)線程使用默認(rèn)的初始堆棧大小, 運(yùn)行在默認(rèn)的優(yōu)先級上。
線程池中有兩種類型的線程:IO線程和非IO線程。IO線程等待在可告警狀態(tài),工作項(xiàng)目作為APC放到IO線程中。如果你的工作項(xiàng)目需要線程執(zhí)行在可警告狀態(tài),你應(yīng)該將它放到IO線程。
非IO工作者線程等待在IO完成端口上,使用非IO線程比IO線程效率更高,也就是說,只要有可能的話,盡量使用非IO線程。IO線程和非IO線程在異步IO操作沒有完成之前都不會(huì)退出。然而,不要在非IO線程中發(fā)出需要很長時(shí)間才能完成的異步IO請求。
正 確使用線程池的方法是,工作項(xiàng)目函數(shù)以及它將會(huì)調(diào)用到的所有函數(shù)都必須是線程池安全的。安全的函數(shù)不應(yīng)該假設(shè)線程是一次性線程的或者是永久線程。一般來 說,應(yīng)該避免使用線程本地存儲(chǔ)和發(fā)出需要永久線程的異步IO調(diào)用,比如說RegNotifyChangeKeyValue函數(shù)。如果需要在永久線程中執(zhí)行 這樣的函數(shù)的話,可以給QueueUserWorkItem傳遞一個(gè)選項(xiàng)WT_EXECUTEINPERSISTENTTHREAD。
注意,線程池不能兼容COM的單線程套間(STA)模型。
為了更深入地講解操作系統(tǒng)實(shí)現(xiàn)的線程池的優(yōu)越性,我們首先嘗試著自己實(shí)現(xiàn)一個(gè)簡單的線程池模型。
代碼如下:
/************************************************************************/
/* 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);
}
我們把工作項(xiàng)目放到一個(gè)隊(duì)列中,用一個(gè)信號量通知線程池,線程池中任意一個(gè)線程取出工作項(xiàng)目來執(zhí)行,執(zhí)行完畢之后,線程返回線程池,繼續(xù)等待新的工作項(xiàng)目。
線程池中線程的數(shù)量是固定的,預(yù)先創(chuàng)建好的,永久的線程,直到銷毀線程池的時(shí)候,這些線程才會(huì)被銷毀。
線程池中線程獲得工作項(xiàng)目的機(jī)會(huì)是均等的,隨機(jī)的,并沒有特別的方式保證哪一個(gè)線程具有特殊的優(yōu)先獲得工作項(xiàng)目的機(jī)會(huì)。
而且,同一時(shí)刻可以并發(fā)運(yùn)行的線程數(shù)目沒有任何限定。事實(shí)上,在我們的執(zhí)行計(jì)算任務(wù)的演示代碼中,所有的線程都并發(fā)執(zhí)行。
下面,我們再來看一下,完成同樣的任務(wù),系統(tǒng)提供的線程池是如何運(yùn)作的。
/************************************************************************/
/* 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);
}
很簡單,是吧?我們僅需要關(guān)注于我們的回調(diào)函數(shù)即可。但是與我們的簡單模擬來比,系統(tǒng)提供的線程池有著更多的優(yōu)點(diǎn)。
首先,線程池中線程的數(shù)目是動(dòng)態(tài)調(diào)整的,其次,線程池利用IO完成端口的特性,它可以限制并發(fā)運(yùn)行的線程數(shù)目,默認(rèn)情況下,將會(huì)限制為CPU的數(shù)目,這可以減少線程切換。它挑選最近執(zhí)行過的線程再次投入執(zhí)行,從而避免了不必要的線程切換。
系統(tǒng)提供的線程池背后的策略,我們下一節(jié)繼續(xù)再談。
參考書目
1, MSDN Library
2, 《Windows高級編程指南》
3, 《Windows核心編程》
4, 《Windows 2000 設(shè)備驅(qū)動(dòng)程序設(shè)計(jì)指南》
正文
異步IO、APC、IO完成端口、線程池與高性能服務(wù)器之五 服務(wù)器的性能指標(biāo)與實(shí)現(xiàn)高性能的途徑
服務(wù)器的性能指標(biāo)
作為一個(gè)網(wǎng)絡(luò)服務(wù)器程序,性能永遠(yuǎn)是第一位的指標(biāo)。性能可以這樣定義:在給定的硬件條件和時(shí)間里,能夠處理的任務(wù)量。能夠最大限度地利用硬件性能的服務(wù)器設(shè)計(jì)才是良好的設(shè)計(jì)。
設(shè)計(jì)良好的服務(wù)器還應(yīng)該考慮平均服務(wù),對于每一個(gè)客戶端,服務(wù)器應(yīng)該給予每個(gè)客戶端平均的服務(wù),不能讓某一個(gè)客戶端長時(shí)間得不到服務(wù)而發(fā)生“饑餓”的狀況。
可伸縮性,也就是說,隨著硬件能力的提高,服務(wù)器的性能能夠隨之呈線性增長。
實(shí)現(xiàn)高性能的途徑
一 個(gè)實(shí)際的服務(wù)器的計(jì)算是很復(fù)雜的,往往是混合了IO計(jì)算和CPU計(jì)算。IO計(jì)算指計(jì)算任務(wù)中以IO為主的計(jì)算模型,比如文件服務(wù)器、郵件服務(wù)器等,混合了 大量的網(wǎng)絡(luò)IO和文件IO;CPU計(jì)算指計(jì)算任務(wù)中沒有或很少有IO,比如加密/解密,編碼/解碼,數(shù)學(xué)計(jì)算等等。
在CPU計(jì)算中,單線 程和多線程模型效果是相當(dāng)?shù)摹!禬in32多線程的性能》中說“在一個(gè)單處理器的計(jì)算機(jī)中,基于 CPU 的任務(wù)的并發(fā)執(zhí)行速度不可能比串行執(zhí)行速度快,但是我們可以看到,在 Windows NT 下線程創(chuàng)建和切換的額外開銷非常小;對于非常短的計(jì)算,并發(fā)執(zhí)行僅僅比串行執(zhí)行慢 10%,而隨著計(jì)算長度的增加,這兩個(gè)時(shí)間就非常接近了。”
可見,對于純粹的CPU計(jì)算來說,如果只有一個(gè)CPU,多線程模型是不合適的。考慮一個(gè)執(zhí)行密集的CPU計(jì)算的服務(wù),如果有幾十個(gè)這樣的線程并發(fā)執(zhí)行,過于頻繁地任務(wù)切換導(dǎo)致了不必要的性能損失。
在 編程實(shí)現(xiàn)上,單線程模型計(jì)算模型對于服務(wù)器程序設(shè)計(jì)是很不方便的。因此,對于CPU計(jì)算采用線程池工作模型是比較恰當(dāng)?shù)摹?QueueUserWorkItem函數(shù)非常適合于將一個(gè)CPU計(jì)算放入線程池。線程池實(shí)現(xiàn)將會(huì)努力減少這種不必要的線程切換,而且控制并發(fā)線程的數(shù)目為 CPU的數(shù)目。
我們真正需要關(guān)心的是IO計(jì)算,一般的網(wǎng)絡(luò)服務(wù)器程序往往伴隨著大量的IO計(jì)算。提高性能的途徑在于要避免等待IO 的結(jié)束,造成CPU空閑,要盡量利用硬件能力,讓一個(gè)或多個(gè)IO設(shè)備與CPU并發(fā)執(zhí)行。前面介紹的異步IO,APC,IO完成端口都可以達(dá)到這個(gè)目的。
對 于網(wǎng)絡(luò)服務(wù)器來說,如果客戶端并發(fā)請求數(shù)目比較少的話,用簡單的多線程模型就可以應(yīng)付了。如果一個(gè)線程因?yàn)榈却齀O操作完成而被掛起,操作系統(tǒng)將會(huì)調(diào)度另 外一個(gè)就緒的線程投入運(yùn)行,從而形成并發(fā)執(zhí)行。經(jīng)典的網(wǎng)絡(luò)服務(wù)器邏輯大多采用多線程/多進(jìn)程方式,在一個(gè)客戶端發(fā)起到服務(wù)器的連接時(shí),服務(wù)器將會(huì)創(chuàng)建一個(gè) 線程,讓這個(gè)新的線程來處理后續(xù)事務(wù)。這種以一個(gè)專門的線程/進(jìn)程來代表一個(gè)客戶端對象的編程方法非常直觀,易于理解。
對于大型網(wǎng)絡(luò)服務(wù) 器程序來說,這種方式存在著局限性。首先,創(chuàng)建線程/進(jìn)程和銷毀線程/進(jìn)程的代價(jià)非常高昂,尤其是在服務(wù)器采用TCP“短連接”方式或UDP方式通訊的情 況下,例如,HTTP協(xié)議中,客戶端發(fā)起一個(gè)連接后,發(fā)送一個(gè)請求,服務(wù)器回應(yīng)了這個(gè)請求后,連接也就被關(guān)閉了。如果采用經(jīng)典方式設(shè)計(jì)HTTP服務(wù)器,那 么過于頻繁地創(chuàng)建線程/銷毀線程對性能造成的影響是很惡劣的。
其次,即使一個(gè)協(xié)議中采取TCP“長連接”,客戶端連上服務(wù)器后就一直保持 此連接,經(jīng)典的設(shè)計(jì)方式也是有弊病的。如果客戶端并發(fā)請求量很高,同一時(shí)刻有很多客戶端等待服務(wù)器響應(yīng)的情況下,將會(huì)有過多的線程并發(fā)執(zhí)行,頻繁的線程切 換將用掉一部分計(jì)算能力。實(shí)際上,如果并發(fā)線程數(shù)目過多的話,往往會(huì)過早地耗盡物理內(nèi)存,絕大部分時(shí)間耗費(fèi)在線程切換上,因?yàn)榫€程切換的同時(shí)也將引起內(nèi)存 調(diào)頁。最終導(dǎo)致服務(wù)器性能急劇下降,
對于一個(gè)需要應(yīng)付同時(shí)有大量客戶端并發(fā)請求的網(wǎng)絡(luò)服務(wù)器來說,線程池是唯一的解決方案。線程池不光能夠避免頻繁地創(chuàng)建線程和銷毀線程,而且能夠用數(shù)目很少的線程就可以處理大量客戶端并發(fā)請求。
值得注意的是,對于一個(gè)壓力不大的網(wǎng)絡(luò)服務(wù)器程序設(shè)計(jì),我們并不推薦以上任何技巧。在簡單的設(shè)計(jì)就能夠完成任務(wù)的情況下,把事情弄得很復(fù)雜是很不明智,很愚蠢的行為。
from:
http://blog.chinaunix.net/u2/67780/showart_2057137.html
原文地址 http://blog.chinaunix.net/u/14774/showart_88161.html