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