转自:http://blog.dream4dev.com/article.asp?id=127
异步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完成端口、線程池與高性能服務器之四 線程池
線程池
下面摘抄於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 下線程創建和切換的額外開銷非常小;對於非常短的計算,並發執行僅僅比串行執行慢 10%,而隨著計算長度的增加,這兩個時間就非常接近了。”
可見,對於純粹的CPU計算來說,如果只有一個CPU,多線程模型是不合適的。考慮一個執行密集的CPU計算的服務,如果有幾十個這樣的線程並發執行,過於頻繁地任務切換導致了不必要的性能損失。
在編程實現上,單線程模型計算模型對於服務器程序設計是很不方便的。因此,對於CPU計算采用線程池工作模型是比較恰當的。 QueueUserWorkItem函數非常適合於將一個CPU計算放入線程池。線程池實現將會努力減少這種不必要的線程切換,而且控制並發線程的數目為 CPU的數目。
我們真正需要關心的是IO計算,一般的網絡服務器程序往往伴隨著大量的IO計算。提高性能的途徑在於要避免等待IO 的結束,造成CPU空閑,要盡量利用硬件能力,讓一個或多個IO設備與CPU並發執行。前面介紹的異步IO,APC,IO完成端口都可以達到這個目的。
對於網絡服務器來說,如果客戶端並發請求數目比較少的話,用簡單的多線程模型就可以應付了。如果一個線程因為等待IO操作完成而被掛起,操作系統將會調度 另外一個就緒的線程投入運行,從而形成並發執行。經典的網絡服務器邏輯大多采用多線程/多進程方式,在一個客戶端發起到服務器的連接時,服務器將會創建一 個線程,讓這個新的線程來處理後續事務。這種以一個專門的線程/進程來代表一個客戶端對象的編程方法非常直觀,易於理解。
對於大型網絡服 務器程序來說,這種方式存在著局限性。首先,創建線程/進程和銷毀線程/進程的代價非常高昂,尤其是在服務器采用TCP“短連接”方式或UDP方式通訊的 情況下,例如,HTTP協議中,客戶端發起一個連接後,發送一個請求,服務器回應了這個請求後,連接也就被關閉了。如果采用經典方式設計HTTP服務器, 那麽過於頻繁地創建線程/銷毀線程對性能造成的影響是很惡劣的。
其次,即使一個協議中采取TCP“長連接”,客戶端連上服務器後就一直保 持此連接,經典的設計方式也是有弊病的。如果客戶端並發請求量很高,同一時刻有很多客戶端等待服務器響應的情況下,將會有過多的線程並發執行,頻繁的線程 切換將用掉一部分計算能力。實際上,如果並發線程數目過多的話,往往會過早地耗盡物理內存,絕大部分時間耗費在線程切換上,因為線程切換的同時也將引起內 存調頁。最終導致服務器性能急劇下降,
對於一個需要應付同時有大量客戶端並發請求的網絡服務器來說,線程池是唯一的解決方案。線程池不光能夠避免頻繁地創建線程和銷毀線程,而且能夠用數目很少的線程就可以處理大量客戶端並發請求。
值得註意的是,對於一個壓力不大的網絡服務器程序設計,我們並不推薦以上任何技巧。在簡單的設計就能夠完成任務的情況下,把事情弄得很復雜是很不明智,很愚蠢的行為。