IOCP編程之基本原理

轉載:http://gamebabyrocksun.blog.163.com/blog/static/57153463201036104134250/

在我的博客之前寫了很多關於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的網絡輸入輸出。

至此對於完成端口的本質原理應該有了一個比較好的理解,尤其是掌握了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而已,它是不會考慮每種語言環境的特殊細節的,它只考慮系統的環境。

好了讓我們繼續打造線程的話題,要創建線程,實際核心就是準備一個線程函數,原型如下:

1、使用CreateThread時:

DWORD WINAPI ThreadProc(LPVOID lpParameter);

2、使用_beginthread時:

void __cdecl ThreadProc( void * pParameter );

3、使用_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的基礎知識算是介紹完了,作爲總結,可以回顧下幾個關鍵步驟:

1、  用CreateIoCompletionPort創建完成端口;

2、  定義IOCP線程池函數,類似消息循環那樣寫一個“死循環”調用GetQueuedCompletionStatus函數,並編寫處理代碼;

3、  創建線程;

4、  將重疊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操作的自定義線程池這一本質。實際編碼時決策性的問題就是理解最大併發數和預創建線程數的意義,並根據實際情況設定一個合理的值。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章