在個人博客以前寫了不少關於IOCP的「行雲流水」似的看了讓人發狂的文章,尤爲是幾篇關於IOCP加線程池文章,更是讓一些功力不夠深厚的初學IOCP者,有種吐血的感受。爲了讓你們可以馬上提高內力修爲,而且迅速的掌握IOCP這個Windows平臺上的乾坤大挪移心法,此次我決定給你們好好補補這個基礎。程序員
要想完全征服IOCP,並應用好IOCP這個模型,首先就讓咱們穿越到遙遠的計算機青銅器時代(以出現PC爲標誌),那時候普通的PC安裝的仍是DOS平臺,微軟公司主要靠這個操做系統在IT界的原始叢林中打拼,在DOS中編寫程序,不得不與不少的硬件直接打交道,而最常操做的硬件無非是鍵盤、聲顯卡、硬盤等等,這些設備都有一個特色就是速度慢,固然是相對於PC平臺核心CPU的速度而言,尤爲是硬盤這個機械電子設備,其速度對於徹底電子化得CPU來講簡直是「相對靜止」的設備。不少時候CPU能夠幹完n件(n>1000)事情的時間中,這些硬件可能尚未完成一件事情,顯然讓CPU和這些硬件同步工做將是一種嚴重的浪費,而且也不太可能,此時,聰明的硬件設計師們發明了一種叫作中斷的操做方式,用以匹配這種速度上的嚴重差別。中斷工做的基本原理就是,CPU首先設置一個相似回調函數的入口地址,其次CPU對某個硬件發出一個指令,此時CPU就去幹別的活計了,最後那個慢的象蝸牛同樣的硬件執行完那個指令後,就通知CPU,讓CPU暫時「中斷」手頭的工做,去調用那個「回調函數」。至此一個完整的中斷調用就結束了。這個模型曾經解決了顯卡與CPU不一樣步的問題,最重要的是解決了硬盤速度與CPU速度嚴重不匹配的問題,並所以還派生出了更有名的DMA(直接內存訪問技術,主要是指慢速硬件能夠讀寫本來只能由CPU直接讀寫的內存)硬盤IO方式。(注意這裏說的中斷工做方式只是中斷工做方式的一種,並非所有,詳細的中斷原理請參閱其它專業文獻。)算法
其實「中斷」方式更像是一種管理模型,好比在一個公司中,若是要老闆時時刻刻盯着員工做事情,那麼除非是超人,不然無人可以勝任,同時對於老闆這個稀缺資源來講也是一種極起嚴重的浪費。更多時候老闆只是發指令給員工,而後員工去執行,而老闆就能夠作別的事情,或者乾脆去打高爾夫休息,當員工完成了任務就會經過電話、短信、甚至e-mail等通知老闆,此時老闆就去完成一個響應過程,好比總結、獎罰、發出新指令等等。由此也看出若是一個公司的「老闆佔用率」(相似CPU佔用率)過高,那麼就說明兩種狀況:要麼是它的員工很高效,單位時間內完成的指令很是多;要麼是公司尚未創建有效的「中斷」響應模型。若是你的公司是後者,那麼你就能夠試着用這個模型改造公司的管理了,由此你能夠晉升到管理層,而不用再去管你的服務端程序有沒有使用IOCP了,呵呵呵。數據庫
若是真的搞明白了這個傳說中的「中斷」操做方式,那麼理解IOCP的基本原理就不費勁了。編程
結束了計算機的青銅時代後,讓咱們穿越到如今這個「計算機蒸汽」時代,(注意不是「計算機IT」時代,由於計算機還無法本身編寫程序讓本身去解決問題)。在現代,Windows幾乎成了PC平臺上的標準系統,而PC平臺上的幾大件仍是沒有太大的變化,除了速度愈來愈快。而由於操做系統的美妙封裝,咱們也不用再去直接同硬件打交道了,固然編寫驅動程序的除外。安全
在Windows平臺上,咱們不斷的調用着WriteFile和ReadFile這些抽象的函數,操做着「文件」這種抽象的信息集合,不少時候調用這些函數時,是以一種「準同步」的方式操做硬件的,好比要向一個文件中寫入1M的信息,只有等到WriteFile函數返回,操做纔算結束,這個過程當中,咱們的程序則相似死機同樣,等待硬盤寫入操做的結束(實際是被系統切換出了當前的CPU時間片)。於此同時,調用了WriteFile的線程則沒法幹別的任何事情。由於整個線程是在以一種稱爲過程化的模型中運行,全部的處理流程所有是線性的。對於程序的流暢編寫來講,線性化的東西是一個很是好的東西,甚至幾乎早期不少標準的算法都是基於程序是過程化得這一假設而設計的。而對於一些多任務、多線程環境來講,這種線性的工做方式會使系統嚴重低效,甚至形成嚴重的浪費,尤爲在現代多核CPU已成爲主流的時候,顯然讓一個CPU內核去等待另外一個CPU內核完成某過後再去工做,是很是愚蠢的一種作法。網絡
面對這種狀況,不少程序員的選擇是多線程,也就是專門讓一個線程去進行讀寫操做,而別的線程繼續工做,以繞開這些看起來像死機同樣的函數,可是這個讀寫線程自己仍是以一種與硬盤同步的方式工做的。然而這並非解決問題的最終方法。咱們能夠想象一個繁忙的數據庫系統,要不斷的讀寫硬盤上的文件,可能在短短的一秒鐘時間就要調用n屢次WriteFile或ReadFile,假設這是一個網站的後臺數據庫,那麼這樣的讀寫操做有時還可能都是較大的數據塊,好比網站的圖片就是比較典型的大塊型數據,這時顯然一個讀寫線程也是忙不過來的,由於頗有可能一個寫操做尚未結束,就會又有讀寫操做請求進入,這時讀寫線程幾乎變成了無響應的一個線程,能夠想象這種狀況下,程序可能幾乎總在癱瘓狀態,全部其它的線程都要等待讀寫操做線程完活。也許你會想多建n個線程來進行讀寫操做,其實這種狀況會更糟糕,由於無論你有多少線程,先不說浪費了多少系統資源,而你讀寫的多是相同的一塊硬盤,只有一條通道,結果依然是同樣的,想象硬盤是獨木橋,而有不少人(線程)等着過橋的情形,你就知道這更是一個糟糕的情形。因此說在慢速的IO面前,多線程每每不是「萬靈丹」。數據結構
面對這種情形,微軟公司爲Windows系統專門創建了一種相似「青銅時代」的中斷方式的模型來解決這個問題。固然,不能再像那個年代那樣直接操做硬件了,須要的是舊瓶裝新酒了。微軟是如何作到的呢,實際仍是經過「回調函數」來解決這個問題的,大體也就是要咱們去實現一個相似回調函數的過程,主要用於處理來自系統的一些輸入輸出操做「完成」的通知,至關於一個「中斷」,而後就能夠在過程當中作輸入輸出完成的一些操做了。好比在IO操做完成後刪除緩衝,繼續發出下一個命令,或者關閉文件,設備等。實際上從邏輯的角度來說,咱們依然能夠按照線性的方法來分析整個過程,只不過這是須要考慮的是兩個不一樣的函數過程之間的線性關係,第一個函數是發出IO操做的調用者,而第二個函數則是在完成IO操做以後的被調用者,。而被調用的這個函數在輸入輸出過程當中是不活動的,也不佔用線程資源,它只是個過程(其實就是個函數,內存中的一段代碼而已)。調用這些函數則須要一個線程的上下文,實際也就是一個函數調用棧,不少時候,系統會借用你進程空間中線程來調用這個過程,固然前提條件是事先將能夠被利用的線程設置成「可警告」狀態,這也是線程可警告狀態的所有意義,也就是大多數內核同步等待函數bAlertable(有些書翻譯作可警告的,我認爲應該理解爲對IO操做是一種「時刻警戒」的狀態)參數被傳遞TRUE值以後的效果。好比:WaitForSingleObjectEx、SleepEx等等。多線程
固然上面說的這種方式實際上是一種「借用線程」的方式,當進程中沒有線程可借,或者可借的線程自己也比較忙碌的時候,會形成嚴重的線程爭用狀況,從而形成總體性能低下,這個方式的侷限性也就顯現出來了。注意「可警告」狀態的線程,並不老是在能夠被借用的狀態,它們自己每每也須要完成一些工做,而它調用一些可以讓它進入等待狀態的函數時,才能夠被系統借用,不然仍是不能被借用的。固然借用線程時由於系統有效的保護了棧環境和寄存器環境,因此被借用的線程再被還回時線程環境是不會被破壞的。併發
鑑於借用的線程的不方便和不專業,咱們更但願經過明確的「建立」一批專門的線程來調用這些回調函數(爲了可以更深刻的理解,能夠將借用的線程想象成出租車,而將專門的線程想象成私家車),所以微軟就發明了IOCP「完成端口」這種線程池模型,注意IOCP本質是一種線程池的模型,固然這個線程池的核心工做就是去調用IO操做完成時的回調函數,這就叫專業!這也是IOCP名字的來由,這就比借用線程的方式要更加高效和專業,由於這些線程是專門建立來作此工做的,因此不用擔憂它們還會去作別的工做,而形成忙碌或不響應回調函數的狀況,另外由於IO操做畢竟是慢速的操做,因此幾個線程就已經足能夠應付成千上萬的輸入輸出完成操做的請求了(還有一個前提就是你的回調函數作的工做要足夠少),因此這個模型的性能是很是高的。也是如今Windows平臺上性能最好的輸入輸出模型。它首先就被用來處理硬盤操做的輸入輸出,同時它也支持郵槽、管道、甚至WinSock的網絡輸入輸出。app
至此對於完成端口的本質原理應該有了一個比較好的理解,尤爲是掌握了IOCP是線程池模型的這一本質,那麼對於以後的IOCP實際應用就不會有太多的疑問了。接下去就讓咱們從實際編程的角度來了解一下IOCP,也爲完全掌握IOCP編程打下堅實的基礎。
要應用IOCP,首先就要咱們建立一個叫作IOCP的內核對象,這須要經過CreateIoCompletionPort這個函數來建立,這個函數的原型以下:
HANDLE WINAPI CreateIoCompletionPort(
__in HANDLE FileHandle,
__in HANDLE ExistingCompletionPort,
__in ULONG_PTR CompletionKey,
__in DWORD NumberOfConcurrentThreads
);
這個函數是個自己具備多重功能的函數(Windows平臺上這樣的函數並很少),須要用不一樣的方式來調用,以實現不一樣的功能,它的第一個功能正如其名字所描述的「Create」,就是建立一個完成端口的內核對象,要讓他完成這個功能,只須要指定NumberOfConcurrentThreads參數便可,前三個參數在這種狀況下是沒有意義的,只須要所有傳遞NULL便可,象下面這樣咱們就建立了一個完成端口的內核對象:
HANDLE hICP = ::CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,1);
這裏首先解釋下爲何第一個參數不是NULL而是INVALID_HANDLE_VALUE,由於第一個參數按照定義是一個文件的句柄,也就是須要IOCP操做的文件句柄,而表明「NULL」文件句柄的實際值是INVALID_HANDLE_VALUE,這是由於NULL實際等於0,而0這個文件句柄被用於特殊用途,因此要用INVALID_HANDLE_VALUE來表明「NULL」意義的文件,INVALID_HANDLE_VALUE的值是-1或者0xFFFFFFFF。
最後一個參數NumberOfConcurrentThreads就有必要好好細細的說說了,由於不少文章中對於這個參數老是說的含糊其辭,不知所云,有些文章中甚至人云亦云的說賦值爲CPU個數的2倍便可,所謂知其然,不知其因此然。其實這個參數的真實含義就是「真正併發同時執行的最大線程數」,這個併發是真併發,怎麼去理解呢,若是你有兩顆CPU,而你賦值爲2那麼就是說,在每顆CPU上執行一個線程,而且真正的併發同時執行,固然若是你設置了比CPU數量更大的數值,它的含義就變成了一個理論併發值,而實際系統的最大可能的嚴格意義上的併發線程數就是CPU個數,也就是你在任務管理器中看到的CPU個數(多是物理個數,也多是內核個數,還有多是超線程個數,或者它們的積)。講到這裏你們也許就有疑問了,爲何有些文章資料中說要設置成CPU個數的2倍呢?這一般是一個半經驗值,由於大多數IOCP完成回調的過程當中,須要一些邏輯處理,有些是業務性的,有些要訪問數據庫,有些還可能訪問硬盤,有些可能須要進行數據顯示等等,不管哪一種處理,這老是要花費時間的,而系統發現你設置了超過CPU個數的併發值時,那麼它就儘量的來回切換這些線程,使他們在一個時間段內看起來像是併發的,好比在1ms的時間週期內,同時有4個IOCP線程被調用,那麼從1ms這段時間來看的話,能夠認爲是有4個線程被併發執行了,固然時間能夠無限被細分,真併發和模擬併發實際就是針對時間細分的粒度來講的。這樣一來如何設置併發數就是個設計決策問題,決策的依據就是你的回調函數究竟要幹些什麼活,若是是時間較長的活計,就要考慮切換其它線程池來完成,若是是等待性質的活計,好比訪問硬盤,等待某個事件等,就能夠設置高一點的併發值,強制系統切換線程形成「僞併發」,若是是很是快速的活計,那麼就直接設置CPU個數的併發數就好了,這時候防止線程頻繁切換是首要任務。固然併發數最好是跟蹤調試一下後再作決定,默認的推薦值就是CPU個數的2倍了。(繞了一大圈我仍是「人云亦云」了一下,哎呦!誰扔的磚頭?!)
上面的所有就是建立一個完成端口對象,接下來就是打造線程了,打造的方法地球人都知道了,就是CreateThread,固然按照人云亦云的說法應該替之以_beginthread或_beginthreadex,緣由嘛?你想知道?真的想知道?好了看你這麼誠懇的看到了這裏,那就告訴你吧,緣由其實就是由於咱們使用的語言從本質上說是C/C++,不少時候咱們須要在線程函數中調用不少的C/C++味很重的庫函數,而有些函數是在Windows誕生之前甚至是多線程多任務誕生之前就誕生了,這些老爺級的函數不少都沒有考慮過多線程安全性,還有就是C++的全局對象靜態對象等都須要調用它們的構造函數來初始化,而調用的主體就是線程,基於這些緣由就要使用C/C++封裝過的建立線程函數來建立線程,而CreateThread始終是Windows系統的API而已,它是不會考慮每種語言環境的特殊細節的,它只考慮系統的環境。
好了讓咱們繼續打造線程的話題,要建立線程,實際核心就是準備一個線程函數,原型以下:
一、使用CreateThread時:
DWORD WINAPI ThreadProc(LPVOID lpParameter);
二、使用_beginthread時:
void __cdecl ThreadProc( void * pParameter );
三、使用_beginthreadex時:
unsigned int __stdcall ThreadProc(void* pParam);
其實上面三個函數原型都是很簡單的,定義一個線程函數並非什麼難事,而真正困難的是對線程的理解和定義一個好的線程函數。這裏我就不在多去談論關於線程原理和如何寫好一個線程函數的內容了,你們能夠去參閱相關的文獻。
如今咱們接着討論IOCP的專用線程如何編寫,IOCP專用線程編寫的核心工做就是調用一個同步函數GetQueuedCompletionStatus,爲了理解的方便性,你能夠想象這個函數的工做原理與那個有名的GetMessage是相似的,雖然這種比喻可能不太確切,可是他們工做方式是有些相似的地方,它們都會使調用它們的線程進入一種等待狀態,只是這個函數不是等待消息隊列中的消息,它是用來等待「被排隊的完成狀態」(就是它名字的含義)的,排隊的完成狀態,其實就是IO操做完成的通知(別告訴我你還不知道什麼是IO操做),若是當前沒有IO完成的通知,那麼這個函數就會讓線程進入「等待狀態」,實際也就是一種「可警告」的狀態,這樣系統線程調度模塊就會登記這個線程,一旦有IO完成通知,系統就會「激活」這個線程,當即分配時間片,讓該線程開始繼續執行,已完成IO完成通知的相關操做。
首先讓我看看GetQueuedCompletionStatus的函數原型:
BOOL WINAPI GetQueuedCompletionStatus(
__in HANDLE CompletionPort,
__out LPDWORD lpNumberOfBytes,
__out PULONG_PTR lpCompletionKey,
__out LPOVERLAPPED* lpOverlapped,
__in DWORD dwMilliseconds
);
第一個參數就是咱們以前建立的那個完成端口內核對象的句柄,這個參數實際也就是告訴系統,咱們當前的線程是歸哪一個完成端口對象來調度。
第二個參數是一個比較有用的參數,在函數返回後它將告訴咱們這一次的IO操做實際傳輸或者接收了多少個字節的信息,這對於咱們校驗數據收發完整性很是有用。
第三個參數是與完成端口句柄綁定的一個一對一的數據指針,固然這個數據是咱們綁到這個完成端口句柄上的,其實這個參數也是相似本人博客文章中所提到的那個「火車頭」的做用的,它的做用和意義就是在咱們獲得完成通知時,能夠拿到咱們在最開初建立完成端口對象時綁定到句柄上的一個自定義的數據。這裏給一個提示就是,在用C++的類封裝中,一般這個參數咱們會在綁定時傳遞類的this指針,而在GetQueuedCompletionStatus返回時又能夠拿到這個類的this指針,從而能夠在這個完成線程中調用類的方法。
第四個參數就是在本人其它IOCP相關博文中詳細介紹過的重疊操做的數據結構,它也是一個火車頭,這裏就不在贅述它的用法了,請你們查閱本人其它博文拙做。
第五個參數是一個等待的毫秒數,也就是GetQueuedCompletionStatus函數等待IO完成通知的一個最大時間長度,若是超過這個時間值,GetQueuedCompletionStatus就會返回,而且返回值一個0值,此時調用GetLastError函數會獲得一個明確的WAIT_TIMEOUT,也就是說它等待超時了,也沒有等到一個IO完成通知。這時咱們能夠作一些相應的處理,而最多見的就是再次調用GetQueuedCompletionStatus函數讓線程進入IO完成通知的等待狀態。固然咱們能夠傳遞一個INFINITE值,表示讓此函數一直等待,直到有一個完成通知進入完成狀態隊列。固然也能夠爲這個參數傳遞0值,表示該函數沒必要等待,直接返回,此時他的工做方式有些相似PeekMessage函數。
函數的參數和原型都搞清楚了,下面就讓咱們來看看調用的例子:
UINT CALLBACK IOCPThread(void* pParam)
{
CoInitialize(NULL);
DWORD dwBytesTrans = 0;
DWORD dwPerData = 0;
LPOVERLAPPED lpOverlapped = NULL;
while(1)
{
BOOL bRet = GetQueuedCompletionStatus( hICP,&dwBytesTrans
,&dwPerData,&lpOverlapped,INFINITE);
if( NULL == lpOverlapped )
{
DWORD dwError = GetLastError();
......//錯誤處理
}
PMYOVERLAPPED pMyOL
= CONTAINING_RECORD(lpOverlapped, MYOVERLAPPED, m_ol);
if( !HasOverlappedIoCompleted(lpOverlapped) )
{//檢測到不是一個真正完成的狀態
DWORD dwError = GetLastError();
......//錯誤處理
}
...... //繼續處理
}
return 0;
}
在這個線程函數中,咱們寫了一個死循環,這個是必要的,由於這個線程要反覆處理IO完成通知的操做。跟咱們常見的消息循環是殊途同歸。
有了線程函數,接着就是建立線程了,對於IOCP來講,建立多少線程實際上是一個決策問題,通常的原則就是建立的實際線程數量,不該小於調用CreateIoCompletionPort建立完成端口對象時指定的那個最大併發線程數。通常的指導原則是:若是完成線程的任務比較繁重大多數狀況下執行的是其它的慢速等待性質的操做(好比磁盤磁帶讀寫操做,數據庫查詢操做,屏幕顯示等)時,因爲這些操做的特色,咱們能夠適當的提升初始建立的線程數量。可是若是是執行計算密集型的操做時(好比網遊服務端的場景變換運算,科學計算,工程運算等等),就不易再靠增長線程數來提升性能,由於這類運算會比較耗費CPU,無法切換出當前CPU時間片,多餘的線程反倒會形成由於頻繁的線程切換而形成整個程序響應性能的降低,此時爲了保證IOCP的響應性,能夠考慮再創建線程池來接力數據專門進行計算,這也是個人博文《IOCP編程之「雙節棍」》篇中介紹的用線程池接力進行計算並提升性能的思想的核心。
下面的例子展現瞭如何建立IOCP線程池中的線程:
SYSTEM_INFO si = {};
GetSystemInfo(&si);
//建立CPU個數個IOCP線程
for( int i = 0; i < si.dwNumberOfProcessors; i ++ )
{
UINT nThreadID = 0;
//以暫停的狀態建立線程狀態
HANDLE hThread = (HANDLE)_beginthreadex(NULL,0,IOCPThread
,(void*)pThreadData,CREATE_SUSPENDED,(UINT*)&nThreadID);
//而後判斷建立是否成功
if( NULL == reinterpret_cast<UINT>(m_hThread)
|| 0xFFFFFFFF == reinterpret_cast<UINT>(m_hThread) )
{//建立線程失敗
......//錯誤處理
}
::ResumeThread(hThread);//啓動線程
}
建立好了IOCP的線程池,就能夠往IOCP線程池中添加用來等待完成的那些重疊IO操做的句柄了,好比:重疊IO方式的文件句柄,重疊IO操做方式的SOCKET句柄,重疊IO操做的命名(匿名)管道等等。上面的這個操做能夠被稱做將句柄綁定到IOCP,綁定的方法就是再次調用CreateIoCompletionPort函數,此次調用時,就須要明確的指定前兩個參數了,例子以下:
//建立一個重疊IO方式的SOCKET
SOCKET skSocket = ::WSASocket(AF_INET,SOCK_STREAM,IPPROTO_IP,
NULL,0,WSA_FLAG_OVERLAPPED);
......//其它操做
//綁定到IOCP
CreateIoCompletionPort((HANDLE)skSocket,hICP,NULL,0);
由代碼就能夠看出這步操做就很是的簡單了,直接再次調用CreateIoCompletionPort函數便可,只是此次調用的意義就不是建立一個完成端口對象了,而是將一個重疊IO方式的對象句柄綁定到已建立好的完成端口對象上。
至此整個IOCP的基礎知識算是介紹完了,做爲總結,能夠回顧下幾個關鍵步驟:
一、 用CreateIoCompletionPort建立完成端口;
二、 定義IOCP線程池函數,相似消息循環那樣寫一個「死循環」調用GetQueuedCompletionStatus函數,並編寫處理代碼;
三、 建立線程;
四、 將重疊IO方式的對象句柄綁定到IOCP上。
只要記住了上面4個關鍵步驟,那麼使用IOCP就基本掌握了。最後做爲補充,讓我再來討論下這個核心步驟以外的一些附帶的步驟。
如今假設咱們已經建立了一個這樣的IOCP線程池,並且這個線程池也工做的很是好了,那麼咱們該如何與這個線程池中的線程進行交互呢?還有就是咱們如何讓這個線程池停下來?
其實這個問題能夠很簡單的來思考,既然IOCP線程池核心的線程函數中有一個相似消息循環的結構,那麼是否是也有一個相似PostMessage之類的函數來向其發送消息,從而實現與IOCP線程的交互呢?答案是確定的,這個函數就是PostQueuedCompletionStatus,如今看到它的名字,你應該已經猜到它的用途了吧?對了,它就是用來向這個相似消息循環的循環中發送自定義的「消息」的,固然,它不是真正的消息,而是一個模擬的「完成狀態」。這個函數的原型以下:
BOOL WINAPI PostQueuedCompletionStatus(
__in HANDLE CompletionPort,
__in DWORD dwNumberOfBytesTransferred,
__in ULONG_PTR dwCompletionKey,
__in LPOVERLAPPED lpOverlapped
);
它的參數與GetQueuedCompletionStatus相似,其實爲了理解上的簡單,咱們能夠認爲PostQueuedCompletionStatus的參數就是原樣的被copy到了GetQueuedCompletionStatus,怎麼調用這個函數就應該能夠理解了。一般在須要中止整個IOCP線程池工做時,就能夠調用這個函數發送一個特殊的標誌,好比設定dwCompletionKey爲NULL,而且在自定義lpOverlapped指針結構以後帶上一個表示關閉的標誌等。這樣在線程函數中就能夠經過斷定這些條件而明確的知道當前線程池須要關閉。固然也能夠定義其它的操做擴展碼來指定IOCP線程池執行指定的操做。下面的例子代碼演示瞭如何發送一個IO完成狀態:
MYOVERLAPPED *pOL = new MYOVERLAPPED ;
.......//其它初始化代碼
pOL->m_iOpCode = OP_CLOSE;//指定關閉操做碼
.......
PostQueuedCompletionStatus(hICP,0,NULL,(LPOVERLAPPED)pOL);
至此IOCP的基礎性的支持算是介紹完了,本篇文章的主要目的是爲了讓你們理解IOCP的本質和工做原理,爲輕鬆駕馭IOCP這個編程模型打下堅實的基礎。最終須要掌握的就是認識到IOCP其實就是一個管理IO操做的自定義線程池這一本質。實際編碼時決策性的問題就是理解最大併發數和預建立線程數的意義,並根據實際狀況設定一個合理的值。