IOCP編程之基本原理_第1頁
IOCP編程之基本原理_第2頁
IOCP編程之基本原理_第3頁
IOCP編程之基本原理_第4頁
IOCP編程之基本原理_第5頁
已閱讀5頁,還剩43頁未讀 繼續(xù)免費(fèi)閱讀

下載本文檔

版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進(jìn)行舉報或認(rèn)領(lǐng)

文檔簡介

1、IOCP編程之基本原理在我的博客之前寫了很多關(guān)于IOCP的行云流水”似的看了讓人發(fā)狂的文章,尤其是幾篇關(guān)于IOCP加線程池文章,更是讓一些功力不夠深厚的初學(xué)IOCP者,有種吐血的感覺。為了讓大家能夠立刻提升內(nèi)力修為,并且迅速的掌握IOCP這個Windows平臺上的乾坤大挪移心法,這次我決定給大家好好補(bǔ)補(bǔ)這個 基礎(chǔ)。要想徹底征服IOCP,并應(yīng)用好IOCP這個模型,首先就讓我們穿越到遙遠(yuǎn)的計算機(jī)青銅器時代(以 出現(xiàn)PC為標(biāo)志),那時候普通的 PC安裝的還是DOS平臺,微軟公司主要靠這個操作系統(tǒng)在IT界的原始叢林中打拼,在DOS中編寫程序,不得不與很多的硬件直接打交道,而最常操作的硬件無非是鍵盤、聲

2、顯卡、硬盤等等,這些設(shè)備都有一個特點(diǎn)就是速度慢,當(dāng)然是相對于PC平臺核心CPU的速度而言,尤其是硬盤這個機(jī)械電子設(shè)備,其速度對于完全電子化得CPU來說簡直是 相對靜止”的設(shè)備。很多時候 CPU可以干完n件(n>1000 )事情的時間中,這些硬件可能還沒有完成一件事情,顯然讓 CPU和這些硬件同 步工作將是一種嚴(yán)重的浪費(fèi),并且也不太可能,此時,聰明的硬件設(shè)計師們發(fā)明了一種叫做中斷的操作方 式,用以匹配這種速度上的嚴(yán)重差異。中斷工作的基本原理就是,CPU首先設(shè)置一個類似回調(diào)函數(shù)的入口地址,其次CPU對某個硬件發(fā)出一個指令,此時 CPU就去干別的活計了,最后那個慢的象蝸牛一樣的硬 件執(zhí)行完那個

3、指令后,就通知CPU ,讓CPU暫時中斷”手頭的工作,去調(diào)用那個 回調(diào)函數(shù)至此一個完整的中斷調(diào)用就結(jié)束了。這個模型曾經(jīng)解決了顯卡與CPU不同步的問題,最重要的是解決了硬盤速度與CPU速度嚴(yán)重不匹配的問題,并因此還派生出了更有名的DMA (直接內(nèi)存訪問技術(shù),主要是指慢速硬件可以讀寫原本只能由 CPU直接讀寫的內(nèi)存)硬盤IO方式。(注意這里說的中斷工作方式只是中斷工作方 式的一種,并不是全部,詳細(xì)的中斷原理請參閱其它專業(yè)文獻(xiàn)。)其實 中斷”方式更像是一種管理模型,比如在一個公司中,如果要老板時時刻刻盯著員工作事情,那 么除非是超人,否則無人能夠勝任,同時對于老板這個稀缺資源來說也是一種極起嚴(yán)重的浪

4、費(fèi)。更多時候 老板只是發(fā)指令給員工,然后員工去執(zhí)行,而老板就可以做別的事情,或者干脆去打高爾夫休息,當(dāng)員工 完成了任務(wù)就會通過電話、短信、甚至 e-mail等通知老板,此時老板就去完成一個響應(yīng)過程,比如總結(jié)、 獎罰、發(fā)出新指令等等。由此也看出如果一個公司的老板占用率”(類似CPU占用率)太高,那么就說明兩種情況:要么是它的員工很高效,單位時間內(nèi)完成的指令非常多;要么是公司還沒有建立有效的中斷”響應(yīng)模型。如果你的公司是后者,那么你就可以試著用這個模型改造公司的管理了,由此你可以晉升到管 理層,而不用再去管你的服務(wù)端程序有沒有使用IOCP 了,呵呵呵。如果真的搞明白了這個傳說中的中斷'操作

5、方式,那么理解IOCP的基本原理就不費(fèi)勁了。結(jié)束了計算機(jī)的青銅時代后,讓我們穿越到現(xiàn)在這個 計算機(jī)蒸汽”時代,(注意不是 計算機(jī)IT時代,因為計算機(jī)還沒法自己編寫程序讓自己去解決問題)。在現(xiàn)代,Windows幾乎成了 PC平臺上的標(biāo)準(zhǔn)系統(tǒng),而PC平臺上的幾大件還是沒有太大的變化,除了速度越來越快。而因為操作系統(tǒng)的美妙封裝,我們也不 用再去直接同硬件打交道了,當(dāng)然編寫驅(qū)動程序的除外。在Windows平臺上,我們不斷的調(diào)用著 WriteFile和ReadFile這些抽象的函數(shù),操作著 文件”這種 抽象的信息集合,很多時候調(diào)用這些函數(shù)時,是以一種準(zhǔn)同步”的方式操作硬件的,比如要向一個文件中寫 入1M

6、的信息,只有等到 WriteFile函數(shù)返回,操作才算結(jié)束,這個過程中,我們的程序則類似死機(jī)一樣,等待硬盤寫入操作的結(jié)束(實際是被系統(tǒng)切換出了當(dāng)前的CPU時間片)。于此同時,調(diào)用了 WriteFile的線程則無法干別的任何事情。因為整個線程是在以一種稱為過程化的模型中運(yùn)行,所有的處理流程全部是 線性的。對于程序的流暢編寫來說,線性化的東西是一個非常好的東西,甚至幾乎早期很多標(biāo)準(zhǔn)的算法都 是基于程序是過程化得這一假設(shè)而設(shè)計的。而對于一些多任務(wù)、多線程環(huán)境來說,這種線性的工作方式會使系統(tǒng)嚴(yán)重低效,甚至造成嚴(yán)重的浪費(fèi),尤其在現(xiàn)代多核 CPU已成為主流的時候,顯然讓一個CPU內(nèi)核去等待另一個CPU內(nèi)核

7、完成某事后再去工作,是非常愚蠢的一種做法。面對這種情況,很多程序員的選擇是多線程, 也就是專門讓一個線程去進(jìn)行讀寫操作,而別的線程繼續(xù)工作,以繞開這些看起來像死機(jī)一樣的函數(shù),但是這個讀寫線程本身還是以一種與硬盤同步的方式工作 的。然而這并不是解決問題的最終方法。我們可以想象一個繁忙的數(shù)據(jù)庫系統(tǒng),要不斷的讀寫硬盤上的文 件,可能在短短的一秒鐘時間就要調(diào)用n多次WriteFile或ReadFile ,假設(shè)這是一個網(wǎng)站的后臺數(shù)據(jù)庫,那么這樣的讀寫操作有時還可能都是較大的數(shù)據(jù)塊,比如網(wǎng)站的圖片就是比較典型的大塊型數(shù)據(jù),這時顯 然一個讀寫線程也是忙不過來的,因為很有可能一個寫操作還沒有結(jié)束,就會又有讀寫

8、操作請求進(jìn)入,這 時讀寫線程幾乎變成了無響應(yīng)的一個線程,可以想象這種情況下,程序可能幾乎總在癱瘓狀態(tài),所有其它 的線程都要等待讀寫操作線程完活。也許你會想多建n個線程來進(jìn)行讀寫操作,其實這種情況會更糟糕,因為不管你有多少線程,先不說浪費(fèi)了多少系統(tǒng)資源,而你讀寫的可能是相同的一塊硬盤,只有一條通道, 結(jié)果依然是一樣的,想象硬盤是獨(dú)木橋,而有很多人(線程)等著過橋的情形,你就知道這更是一個糟糕 的情形。所以說在慢速的IO面前,多線程往往不是 萬靈丹面對這種情形,微軟公司為 Windows系統(tǒng)專門建立了一種類似青銅時代”的中斷方式的模型來解決 這個問題。當(dāng)然,不能再像那個年代那樣直接操作硬件了,需要

9、的是舊瓶裝新酒了。微軟是如何做到的呢, 實際還是通過回調(diào)函數(shù)”來解決這個問題的,大致也就是要我們?nèi)崿F(xiàn)一個類似回調(diào)函數(shù)的過程,主要用于處理來自系統(tǒng)的一些輸入輸出操作完成”的通知,相當(dāng)于一個 中斷”,然后就可以在過程中做輸入輸出完成的一些操作了。比如在IO操作完成后刪除緩沖,繼續(xù)發(fā)出下一個命令,或者關(guān)閉文件,設(shè)備等。實際上從 邏輯的角度來講,我們依然可以按照線性的方法來分析整個過程,只不過這是需要考慮的是兩個不同的函 數(shù)過程之間的線性關(guān)系,第一個函數(shù)是發(fā)出IO操作的調(diào)用者,而第二個函數(shù)則是在完成IO操作之后的被調(diào)用者,。而被調(diào)用的這個函數(shù)在輸入輸出過程中是不活動的,也不占用線程資源,它只是個過程

10、(其實 就是個函數(shù),內(nèi)存中的一段代碼而已)。調(diào)用這些函數(shù)則需要一個線程的上下文,實際也就是一個函數(shù)調(diào) 用棧,很多時候,系統(tǒng)會借用你進(jìn)程空間中線程來調(diào)用這個過程,當(dāng)然前提條件是事先將可以被利用的線 程設(shè)置成可警告”狀態(tài),這也是線程可警告狀態(tài)的全部意義,也就是大多數(shù)內(nèi)核同步等待函數(shù)bAlertable (有些書翻譯做可警告的, 我認(rèn)為應(yīng)該理解為對IO操作是一種 時刻警惕”的狀態(tài))參數(shù)被傳遞TRUE值之后的 效果。比如: WaitForSingleObjectEx 、SleepEx 等等。當(dāng)然上面說的這種方式其實是一種借用線程”的方式,當(dāng)進(jìn)程中沒有線程可借,或者可借的線程本身也比較忙碌的時候,會造成

11、嚴(yán)重的線程爭用情況,從而造成整體性能低下,這個方式的局限性也就顯現(xiàn)出 來了。注意可警告”狀態(tài)的線程,并不總是在可以被借用的狀態(tài),它們本身往往也需要完成一些工作,而它調(diào)用一些能夠讓它進(jìn)入等待狀態(tài)的函數(shù)時,才可以被系統(tǒng)借用,否則還是不能被借用的。當(dāng)然借用線程時 因為系統(tǒng)有效的保護(hù)了棧環(huán)境和寄存器環(huán)境,所以被借用的線程再被還回時線程環(huán)境是不會被破壞的。鑒于借用的線程的不方便和不專業(yè),我們更希望通過明確的創(chuàng)建”一批專門的線程來調(diào)用這些回調(diào)函數(shù)(為了能夠更深入的理解,可以將借用的線程想象成出租車,而將專門的線程想象成私家車),因此微 軟就發(fā)明了 IOCP“完成端口 ”這種線程池模型,注意IOCP本質(zhì)是一

12、種線程池的模型,當(dāng)然這個線程池的核 心工作就是去調(diào)用IO操作完成時的回調(diào)函數(shù),這就叫專業(yè)!這也是 IOCP名字的來由,這就比借用線程的 方式要更加高效和專業(yè),因為這些線程是專門創(chuàng)建來做此工作的,所以不用擔(dān)心它們還會去做別的工作, 而造成忙碌或不響應(yīng)回調(diào)函數(shù)的情況,另外因為IO操作畢竟是慢速的操作,所以幾個線程就已經(jīng)足可以應(yīng) 付成千上萬的輸入輸出完成操作的請求了(還有一個前提就是你的回調(diào)函數(shù)做的工作要足夠少),所以這個模型的性能是非常高的。也是現(xiàn)在 Windows平臺上性能最好的輸入輸出模型。它首先就被用來處理硬盤操作的輸入輸出,同時它也支持郵槽、管道、甚至 WinSock的網(wǎng)絡(luò)輸入輸出。至此對

13、于完成端口的本質(zhì)原理應(yīng)該有了一個比較好的理解,尤其是掌握了 IOCP是線程池模型的這一本質(zhì),那么對于之后的IOCP實際應(yīng)用就不會有太多的疑問了。接下去就讓我們從實際編程的角度來了解 一下IOCP ,也為徹底掌握IOCP編程打下堅實的基礎(chǔ)。要應(yīng)用IOCP,首先就要我們創(chuàng)建一個叫做IOCP的內(nèi)核對象,這需要通過 CreateIoCompletionPort這個函數(shù)來創(chuàng)建,這個函數(shù)的原型如下:HANDLE WINAPI CreateIoCompletionPort(_inHANDLE FileHandle,_inHANDLE ExistingCompletionPort,_inULONG_PTR C

14、ompletionKey,_inDWORD NumberOfConcurrentThreads);這個函數(shù)是個本身具有多重功能的函數(shù)( Windows平臺上這樣的函數(shù)并不多),需要用不同的方式 來調(diào)用,以實現(xiàn)不同的功能,它的第一個功能正如其名字所描述的“Create:就是創(chuàng)建一個完成端口的內(nèi)核對象,要讓他完成這個功能,只需要指定NumberOfConcurrentThreads參數(shù)即可,前三個參數(shù)在這種情況下是沒有意義的,只需要全部傳遞NULL即可,象下面這樣我們就創(chuàng)建了一個完成端口的內(nèi)核對象:HANDLE hICP =CreateIoCompletionPort(INVALID_HANDLE

15、_VALUE,NULL,0,1);這里首先解釋下為什么第一個參數(shù)不是 NULL而是INVALID_HANDLE_VALUE ,因為第一個參數(shù)按 照定義是一個文件的句柄,也就是需要IOCP操作的文件句柄,而代表“NULL文件句柄的實際值是INVALID_HANDLE_VALUE ,這是因為NULL實際等于0,而0這個文件句柄被用于特殊用途,所以要用 INVALID_HANDLE_VALUE 來代表 “ NULL1義的文件,INVALID_HANDLE_VALUE 的值是-1 或者 0XFFFFFFFF 。最后一個參數(shù)NumberOfConcurrentThreads 就有必要好好細(xì)細(xì)的說說了,

16、因為很多文章中對于這個 參數(shù)總是說的含糊其辭,不知所云,有些文章中甚至人云亦云的說賦值為CPU個數(shù)的2倍即可,所謂知其然,不知其所以然。其實這個參數(shù)的真實含義就是真正并發(fā)同時執(zhí)行的最大線程數(shù) ”,這個并發(fā)是真并發(fā),怎么去理解呢,如果你有兩顆 CPU ,而你賦值為2那么就是說,在每顆 CPU上執(zhí)行一個線程,并且真正 的并發(fā)同時執(zhí)行,當(dāng)然如果你設(shè)置了比 CPU數(shù)量更大的數(shù)值,它的含義就變成了一個理論并發(fā)值,而實際系統(tǒng)的最大可能的嚴(yán)格意義上的并發(fā)線程數(shù)就是CPU個數(shù),也就是你在任務(wù)管理器中看到的CPU個數(shù)(可能是物理個數(shù),也可能是內(nèi)核個數(shù),還有可能是超線程個數(shù),或者它們的積)。講到這里大家也許就有疑

17、 問了,為什么有些文章資料中說要設(shè)置成CPU個數(shù)的2倍呢?這通常是一個半經(jīng)驗值,因為大多數(shù)IOCP完成回調(diào)的過程中,需要一些邏輯處理,有些是業(yè)務(wù)性的,有些要訪問數(shù)據(jù)庫,有些還可能訪問硬盤,有 些可能需要進(jìn)行數(shù)據(jù)顯示等等,無論哪種處理,這總是要花費(fèi)時間的,而系統(tǒng)發(fā)現(xiàn)你設(shè)置了超過 CPU個數(shù)的并發(fā)值時,那么它就盡可能的來回切換這些線程,使他們在一個時間段內(nèi)看起來像是并發(fā)的,比如在1ms的時間周期內(nèi),同時有 4個IOCP線程被調(diào)用,那么從1ms這段時間來看的話,可以認(rèn)為是有4個線程被并發(fā)執(zhí)行了,當(dāng)然時間可以無限被細(xì)分,真并發(fā)和模擬并發(fā)實際就是針對時間細(xì)分的粒度來說的。這樣一 來如何設(shè)置并發(fā)數(shù)就是個設(shè)

18、計決策問題,決策的依據(jù)就是你的回調(diào)函數(shù)究竟要干些什么活,如果是時間較 長的活計,就要考慮切換其它線程池來完成,如果是等待性質(zhì)的活計,比如訪問硬盤,等待某個事件等, 就可以設(shè)置高一點(diǎn)的并發(fā)值,強(qiáng)制系統(tǒng)切換線程造成偽并發(fā)”,如果是非常快速的活計,那么就直接設(shè)置CPU個數(shù)的并發(fā)數(shù)就行了,這時候防止線程頻繁切換是首要任務(wù)。當(dāng)然并發(fā)數(shù)最好是跟蹤調(diào)試一下后再做決定,默認(rèn)的推薦值就是 CPU個數(shù)的2倍了。(繞了一大圈我還是 入云亦云”了一下,哎呦!誰扔的磚頭? !)上面的全部就是創(chuàng)建一個完成端口對象,接下來就是打造線程了,打造的方法地球人都知道了,就是CreateThread,當(dāng)然按照人云亦云的說法應(yīng)該替之

19、以_beginthread 或_beginthreadex,原因嘛?你想知道?真的想知道?好了看你這么誠懇的看到了這里,那就告訴你吧,原因其實就是因為我們使用的語言從本質(zhì) 上說是C/C+ ,很多時候我們需要在線程函數(shù)中調(diào)用很多的C/C+味很重的庫函數(shù),而有些函數(shù)是在Windows誕生以前甚至是多線程多任務(wù)誕生以前就誕生了,這些老爺級的函數(shù)很多都沒有考慮過多線程安全性,還有就是C+的全局對象靜態(tài)對象等都需要調(diào)用它們的構(gòu)造函數(shù)來初始化,而調(diào)用的主體就是線程,基于這些原因就要使用 C/C+封裝過的創(chuàng)建線程函數(shù)來創(chuàng)建線程,而CreateThread始終是Windows系統(tǒng)的API而已,它是不會考慮每種

20、語言環(huán)境的特殊細(xì)節(jié)的,它只考慮系統(tǒng)的環(huán)境。好了讓我們繼續(xù)打造線程的話題,要創(chuàng)建線程,實際核心就是準(zhǔn)備一個線程函數(shù),原型如下:1、使用 CreateThread 時:DWORD WINAPI ThreadProc(LPVOID lpParameter);2、使用 _beginthread 時:void _cdecl ThreadProc( void * pParameter );3、使用 _beginthreadex 時:unsigned int _stdcall ThreadProc(void* pParam);其實上面三個函數(shù)原型都是很簡單的,定義一個線程函數(shù)并不是什么難事,而真正困難的是對

21、線程的理解和定義一個好的線程函數(shù)。這里我就不在多去談?wù)撽P(guān)于線程原理和如何寫好一個線程函數(shù)的內(nèi)容了, 大家可以去參閱相關(guān)的文獻(xiàn)。現(xiàn)在我們接著討論IOCP的專用線程如何編寫,IOCP專用線程編寫的核心工作就是調(diào)用一個同步函 數(shù)GetQueuedCompletionStatus ,為了理解的方便性,你可以想象這個函數(shù)的工作原理與那個有名的 GetMessage是類似的,雖然這種比喻可能不太確切,但是他們工作方式是有些類似的地方,它們都會使 調(diào)用它們的線程進(jìn)入一種等待狀態(tài),只是這個函數(shù)不是等待消息隊列中的消息,它是用來等待被排隊的完成狀態(tài)”(就是它名字的含義)的,排隊的完成狀態(tài),其實就是 IO操作完成

22、的通知(別告訴我你還不知道 什么是IO操作),如果當(dāng)前沒有IO完成的通知,那么這個函數(shù)就會讓線程進(jìn)入等待狀態(tài)”,實際也就是一種 可警告”的狀態(tài),這樣系統(tǒng)線程調(diào)度模塊就會登記這個線程,一旦有IO完成通知,系統(tǒng)就會 激活”這個線程,立即分配時間片,讓該線程開始繼續(xù)執(zhí)行,已完成IO完成通知的相關(guān)操作。首先讓我看看GetQueuedCompletionStatus的函數(shù)原型:BOOL WINAPI GetQueuedCompletionStatus(_inHANDLE CompletionPort,_outLPDWORD lpNumberOfBytes,_outPULONG_PTR lpComplet

23、ionKey,_outLPOVERLAPPED* lpOverlapped,_inDWORD dwMilliseconds);第一個參數(shù)就是我們之前創(chuàng)建的那個完成端口內(nèi)核對象的句柄,這個參數(shù)實際也就是告訴系統(tǒng),我們當(dāng)前的線程是歸哪個完成端口對象來調(diào)度。第二個參數(shù)是一個比較有用的參數(shù),在函數(shù)返回后它將告訴我們這一次的IO操作實際傳輸或者接收了多少個字節(jié)的信息,這對于我們校驗數(shù)據(jù)收發(fā)完整性非常有用。第三個參數(shù)是與完成端口句柄綁定的一個一對一的數(shù)據(jù)指針,當(dāng)然這個數(shù)據(jù)是我們綁到這個完成端口句柄上的,其實這個參數(shù)也是類似本人博客文章中所提到的那個火車頭”的作用的,它的作用和意義就是在我們得到完成通知時,

24、可以拿到我們在最開初創(chuàng)建完成端口對象時綁定到句柄上的一個自定義的數(shù)據(jù)。這 里給一個提示就是,在用C+的類封裝中,通常這個參數(shù)我們會在綁定時傳遞類的this指針,而在GetQueuedCompletionStatus返回時又可以拿到這個類的this指針,從而可以在這個完成線程中調(diào)用類的方法。第四個參數(shù)就是在本人其它IOCP相關(guān)博文中詳細(xì)介紹過的重疊操作的數(shù)據(jù)結(jié)構(gòu),它也是一個火車頭,這里就不在贅述它的用法了,請大家查閱本人其它博文拙作。第五個參數(shù)是一個等待的毫秒數(shù),也就是GetQueuedCompletionStatus函數(shù)等待IO完成通知的一個最大時間長度,如果超過這個時間值,GetQueued

25、CompletionStatus就會返回,并且返回值一個0值,此時調(diào)用GetLastError函數(shù)會得到一個明確的 WAIT_TIMEOUT ,也就是說它等待超時了,也沒有等到一 個IO完成通知。這時我們可以做一些相應(yīng)的處理,而最常見的就是再次調(diào)用GetQueuedCompletionStatus函數(shù)讓線程進(jìn)入IO完成通知的等待狀態(tài)。當(dāng)然我們可以傳遞一個INFINITE值,表示讓此函數(shù)一直等待,直到有一個完成通知進(jìn)入完成狀態(tài)隊列。當(dāng)然也可以為這個參數(shù)傳遞0值,表示該函數(shù)不必等待,直接返回,此時他的工作方式有些類似PeekMessage函數(shù)。函數(shù)的參數(shù)和原型都搞清楚了,下面就讓我們來看看調(diào)用的例

26、子: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 = GetLastErr

27、or();/錯誤處理PMYOVERLAPPED pMyOL= CONTAINING_RECORD(lpOverlapped, MYOVERLAPPED, m_ol);if( !HasOverlappedIoCompleted(lpOverlapped) )/檢測到不是一個真正完成的狀態(tài)DWORD dwError = GetLastError();/錯誤處理/繼續(xù)處理return 0;在這個線程函數(shù)中,我們寫了一個死循環(huán),這個是必要的,因為這個線程要反復(fù)處理IO完成通知的操作。跟我們常見的消息循環(huán)是異曲同工。有了線程函數(shù),接著就是創(chuàng)建線程了,對于 IOCP來說,創(chuàng)建多少線程其實是一個決策問題,一

28、般的原則就是創(chuàng)建的實際線程數(shù)量,不應(yīng)小于調(diào)用CreateloCompletionPort 創(chuàng)建完成端口對象時指定的那個最大并發(fā)線程數(shù)。一般的指導(dǎo)原則是:如果完成線程的任務(wù)比較繁重大多數(shù)情況下執(zhí)行的是其它的慢速等待性質(zhì)的操作(比如磁盤磁帶讀寫操作,數(shù)據(jù)庫查詢操作,屏幕顯示等)時,由于這些操作的特點(diǎn),我們可以適當(dāng)?shù)奶岣叱跏紕?chuàng)建的線程數(shù)量。但是如果是執(zhí)行計算密集型的操作時(比如網(wǎng)游服務(wù)端的場景變換運(yùn)算,科學(xué)計算,工程運(yùn)算等等),就不易再靠增加線程數(shù)來提高性能,因為這類運(yùn)算會比較耗費(fèi)CPU ,沒法切換出當(dāng)前CPU時間片,多余的線程反倒會造成因為頻繁的線程切換而造成整個程序響應(yīng)性能的下降,此時為了保證I

29、OCP的響應(yīng)性,可以考慮再建立線程池來接力數(shù)據(jù)專門進(jìn)行計算,這也是我的博文 IOCP 編程之 雙節(jié)棍”篇中介紹的用線程池接力進(jìn)行計算并提高性能的思想的核心。下面的例子展示了如何創(chuàng)建IOCP線程池中的線程:SYSTEM_INFO si = ;GetSystemInfo(&si);/創(chuàng)建CPU個數(shù)個IOCP線程for( int i = 0; i < si.dwNumberOfProcessors; i + )UINT nThreadID = 0;以暫停的狀態(tài)創(chuàng)建線程狀態(tài)HANDLE hThread = (HANDLE)_beginthreadex(NULL,0,IOCPThread,

30、(void*)pThreadData,CREATE_SUSPENDED,(UINT*)&nThreadID);然后判斷創(chuàng)建是否成功if( NULL = reinterpret_cast<UINT>(m_hThread)| 0xFFFFFFFF = reinterpret_cast<UINT>(m_hThread) )創(chuàng)建線程失敗/錯誤處理ResumeThread(hThread);/ 啟動線程創(chuàng)建好了 IOCP的線程池,就可以往IOCP線程池中添加用來等待完成的那些重疊IO操作的句柄了,比如:重疊IO方式的文件句柄,重疊IO操作方式的SOCKET句柄,重疊IO操

31、作的命名(匿名)管道等 等。上面的這個操作可以被稱作將句柄綁定到IOCP ,綁定的方法就是再次調(diào)用CreateIoCompletionPort函數(shù),這次調(diào)用時,就需要明確的指定前兩個參數(shù)了,例子如下:/創(chuàng)建一個重疊IO方式的SOCKETSOCKET skSocket = :WSASocket(AF_INET,SOCK_STREAM,IPPROTO_IP,NULL,0,WSA_FLAG_OVERLAPPED);/其它操作綁定到IOCPCreateIoCompletionPort(HANDLE)skSocket,hICP,NULL,0);由代碼就可以看出這步操作就非常的簡單了,直接再次調(diào)用Crea

32、teIoCompletionPort 函數(shù)即可,只是這次調(diào)用的意義就不是創(chuàng)建一個完成端口對象了,而是將一個重疊IO方式的對象句柄綁定到已創(chuàng)建好的完成端口對象上。至此整個IOCP的基礎(chǔ)知識算是介紹完了,作為總結(jié),可以回顧下幾個關(guān)鍵步驟:1、 用 CreateIoCompletionPort 創(chuàng)建完成端口;2、 定義IOCP線程池函數(shù),類似消息循環(huán)那樣寫一個死循環(huán)”調(diào)用GetQueuedCompletionStatus函數(shù),并編寫處理代碼;3、 創(chuàng)建線程;4、 將重疊IO方式的對象句柄綁定到IOCP上。只要記住了上面4個關(guān)鍵步驟,那么使用IOCP就基本掌握了。最后作為補(bǔ)充,讓我再來討論下這個 核心

33、步驟之外的一些附帶的步驟。現(xiàn)在假設(shè)我們已經(jīng)創(chuàng)建了一個這樣的IOCP線程池,而且這個線程池也工作的非常好了,那么我們該如何與這個線程池中的線程進(jìn)行交互呢?還有就是我們?nèi)绾巫屵@個線程池停下來?其實這個問題可以很簡單的來思考,既然IOCP線程池核心的線程函數(shù)中有一個類似消息循環(huán)的結(jié)構(gòu),那么是不是也有一個類似 PostMessage之類的函數(shù)來向其發(fā)送消息,從而實現(xiàn)與IOCP線程的交互呢? 答案是肯定的,這個函數(shù)就是PostQueuedCompletionStatus ,現(xiàn)在看到它的名字,你應(yīng)該已經(jīng)猜到它的用途了吧?對了,它就是用來向這個類似消息循環(huán)的循環(huán)中發(fā)送自定義的消息”的,當(dāng)然,它不是真正的消息

34、,而是一個模擬的 完成狀態(tài)這個函數(shù)的原型如下:BOOL WINAPI PostQueuedCompletionStatus(_inHANDLE CompletionPort,_inDWORD dwNumberOfBytesTransferred,_inULONG_PTR dwCompletionKey,_inLPOVERLAPPED lpOverlapped);它的參數(shù)與 GetQueuedCompletionStatus 類似,其實為了理解上的簡單,我們可以認(rèn)為 PostQueuedCompletionStatus的參數(shù)就是原樣的被 copy 至U了 GetQueuedCompletionS

35、tatus ,怎么調(diào)用這個函數(shù)就應(yīng)該可以理解了。通常在需要停止整個IOCP線程池工作時,就可以調(diào)用這個函數(shù)發(fā)送一個特殊的標(biāo)志,比如設(shè)定 dwCompletionKey 為NULL ,并且在自定義lpOverlapped 指針結(jié)構(gòu)之后帶上一個表示 關(guān)閉的標(biāo)志等。這樣在線程函數(shù)中就可以通過判定這些條件而明確的知道當(dāng)前線程池需要關(guān)閉。當(dāng)然也可以定義其它的操作擴(kuò)展碼來指定IOCP線程池執(zhí)行指定的操作。下面的例子代碼演示了如何發(fā)送一個IO完成狀態(tài):MYOVERLAPPED *pOL = new MYOVERLAPPED ;./其它初始化代碼pOL->m_iOpCode = OP_CLOSE;/指定

36、關(guān)閉操作碼PostQueuedCompletionStatus(hICP,0,NULL,(LPOVERLAPPED)pOL);至此IOCP的基礎(chǔ)性的支持算是介紹完了,本篇文章的主要目的是為了讓大家理解IOCP的本質(zhì)和工作原理,為輕松駕馭IOCP這個編程模型打下堅實的基礎(chǔ)。最終需要掌握的就是認(rèn)識到IOCP其實就是一個管理IO操作的自定義線程池這一本質(zhì)。實際編碼時決策性的問題就是理解最大并發(fā)數(shù)和預(yù)創(chuàng)建線程數(shù)的 意義,并根據(jù)實際情況設(shè)定一個合理的值。IOCP+WinSock2 新函數(shù)打造高性能 SOCKET池在前一篇文章 WinSock2編程之打造完整的 SOCKET池»中,介紹了 Win

37、Sock2的一些新函數(shù), 并重點(diǎn)詳細(xì)介紹了什么是 SOCKET池,有了這個概念,現(xiàn)在就接著展開更深入的討論。首先這里要重點(diǎn)重申一下就是,SOCKET池主要指的是使用面向連接的協(xié)議的情況下,最常用的就是需要管理大量的 TCP連接的時候。常見的就是 Web服務(wù)器、FTP服務(wù)器等。下面就分步驟的詳細(xì)介紹如何最終實現(xiàn)SOCKET池一、WinSock2 環(huán)境的初始化:要使用WinSock2就需要先初始化Socket2.0的環(huán)境,不廢話,上代碼:WSADATA wd = 0;int iError = WSAStartup(MAKEWORD(2,0), &wd);if( 0 != iError )出

38、現(xiàn)錯誤,最好跟蹤看下錯誤碼是多少return FALSE;if ( LOBYTE(lpwsaData->wVersion) != 2 )非2.0以上環(huán)境退出了事可能是可憐的WinCE系統(tǒng)WSACleanup();return FALSE;最后再不使用WinSock之后都要記得調(diào)用一下 WSACleanup()這個函數(shù);二、裝載 WinSock2 函數(shù):上一篇文章中給出了一個裝載WinSock2函數(shù)的類,這里分解介紹下裝載的具體過程,要提醒的就是,凡是類里面演示了動態(tài)裝載的函數(shù),最好都像那樣動態(tài)載入,然后再調(diào)用。以免出現(xiàn)上網(wǎng)發(fā)帖跪求高手賜 教為什么AcceptEx函數(shù)無法編譯通過等問題。看

39、完這篇文章詳細(xì)你不會再去發(fā)帖找答案了,呵呵呵,好 了,上代碼:/定義一個好用的載入函數(shù)摘自CGRSMsSockFun 類BOOL LoadWSAFun(GUID&funGuid,void*& pFun)本函數(shù)利用參數(shù)返回函數(shù)指針DWORD dwBytes = 0;pFun = NULL;隨便創(chuàng)建一個SOCKET供WSAIoctl使用并不一定要像下面這樣創(chuàng)建SOCKET skTemp =WSASocket(AF_INET,SOCK_STREAM, IPPROTO_TCP, NULL,0, WSA_FLAG_OVERLAPPED);if(INVALID_SOCKET = skTem

40、p)/通常表示沒有正常的初始化 WinSock環(huán)境return FALSE;:WSAIoctl(skTemp, SIO_GET_EXTENSION_FUNCTION_POINTER,&funGuid,sizeof(funGuid),&pFun,sizeof(pFun), &dwBytes, NULL,NULL);:closesocket(skTemp);return NULL != pFun;演示如何動態(tài)載入 AcceptEx函數(shù)LPFN_ACCEPTEX pfnAcceptEx; /首先聲明函數(shù)指針GUID GuidAcceptEx = WSAID_ACCEPTEX;

41、載入LoadWSAFun(GuidAcceptEx,(void*&)pfnAcceptEx); /使用豐富的參數(shù)調(diào)用pfnAcceptEx(sListenSocket,sAcceptSocket,lpOutputBuffer,dwReceiveDataLength,dwLocalAddressLength,dwRemoteAddressLength, lpdwBytesReceived,lpOverlapped);或者:SOCKET skAccept =WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,NULL, 0,WSA_FLAG_OVERLAPP

42、ED);PVOID pBuf = new BYTEsizeof(sockaddr_in) + 16;pfnAcceptEx(skServer, skAccept,pBuf,0,/將接收緩沖置為0,令A(yù)cceptEx直接返回,防止拒絕服務(wù)攻擊sizeof(sockaddr_in) + 16, sizeof(sockaddr_in) + 16, NULL,(LPOVERLAPPED)pAcceptOL);以上是一個簡單的演示,如何動態(tài)載入一個WinSock2擴(kuò)展函數(shù),并調(diào)用之,其它函數(shù)的詳細(xì)例子可以看前一篇文章中CGRSMsSockFun 類的實現(xiàn)部分。如果使用CGRSMsSockFun 類的話當(dāng)

43、然更簡單,像下面這樣調(diào)用即可:CGRSMsSockFun MsSockFun ;MsSockFun.AcceptEx(skServer, skAccept,pBuf,0,/將接收緩沖置為0,令A(yù)cceptEx直接返回,防止拒絕服務(wù)攻擊sizeof(sockaddr_in) + 16, sizeof(sockaddr_in) + 16, NULL,(LPOVERLAPPED)pAcceptOL);如果要使用這個類,那么需要一些修改,主要是異常處理部分,自己注釋掉,或者用其它異常代替掉 即可,這個對于有基礎(chǔ)的讀者來說不是什么難事。三、定義OVERLAPPED 結(jié)構(gòu):要想“IOCP"就要自

44、定義OVERLAPPED ,這是徹底玩轉(zhuǎn)IOCP的不二法門,可以這么說:江湖上有多少種自定義的 OVERLAPPED 派生結(jié)構(gòu)體,就有多少種 IOCP的封裝! ”O(jiān)VERLAPPED 本身是Windows IOCP機(jī)制內(nèi)部需要的一個結(jié)構(gòu)體, 主要用于記錄每個IO操作的 完 成狀態(tài)”,其內(nèi)容對于調(diào)用者來說是沒有意義的,但是很多時候我們把它當(dāng)做一個火車頭”,因為它可以方便的把每個IO操作的相關(guān)數(shù)據(jù)簡單的從調(diào)用處運(yùn)輸?shù)酵瓿苫卣{(diào)函數(shù)中",這是一個非常有用的特性,哪么 如何讓這個火車頭發(fā)揮運(yùn)輸?shù)淖饔媚兀科鋵嵑芎唵危鹤屗蔀橐粋€自定義的更大結(jié)構(gòu)體的第一個成員。然 后用強(qiáng)制類型轉(zhuǎn)換,將自定義的結(jié)構(gòu)

45、體轉(zhuǎn)換成OVERLAPPED指針即可。當(dāng)然不一定非要是新結(jié)構(gòu)體的第一個成員,也可以是任何第n個成員,這時使用VC頭文件中預(yù)定義的一個宏 CONTAINING_RECORD 再 反轉(zhuǎn)回來即可。說到這里一些C+基礎(chǔ)差一點(diǎn)的讀者估計已經(jīng)很頭暈了,更不知道我再說什么,那么我就將好人做 到底吧,來解釋下這個來龍去脈。首先就以我們將要使用的 AcceptEx函數(shù)為例子看看它的原型吧(知道孫悟空的火眼金睛用來干嘛的 嗎?就是用來看原型的,哈哈哈):BOOL AcceptEx(_inSOCKET sListenSocket,_inSOCKET sAcceptSocket,_inPVOID lpOutputBu

46、ffer,_inDWORD dwReceiveDataLength,_inDWORD dwLocalAddressLength,_inDWORD dwRemoteAddressLength,_out LPDWORD lpdwBytesReceived,_inLPOVERLAPPED lpOverlapped);注意最后一個參數(shù),是一個OVERLAPPED 結(jié)構(gòu)體的指針(LP的意思是Long Pointer,即指向32位地址長指針,注意不是 老婆”拼音的縮寫),本身這個參數(shù)的意思就是分配一塊OVERLAPPED大小的內(nèi)存,在IOCP調(diào)用方式下傳遞給 AcceptEx函數(shù)用,調(diào)用者不用去關(guān)心里面的

47、任何內(nèi)容,而在完成過程中(很多時候是另一個線程中的事情了),通常調(diào)用GetQueuedCompletionStatus函數(shù)后,會再次得到這個指針,接著讓我們也看看它的原型:BOOL WINAPI GetQueuedCompletionStatus(_inHANDLE CompletionPort,_outLPDWORD lpNumberOfBytes,_outPULONG_PTR lpCompletionKey,_outLPOVERLAPPED* lpOverlapped,_inDWORD dwMilliseconds);注意這里的LPOVERLAPPED 多了一個*變成了指針的指針,并且前面

48、的說明很清楚 Out!很明白了 吧,不明白就真的 Out 了。這里就可以重新得到調(diào)用 AcceptEx傳入的LPOVERLAPPED 指針,也就是得 到了這個火車頭”,因為只是一個指針,并沒有詳細(xì)的限定能有多大,所以可以在火車頭的后面放很多東西。再仔細(xì)觀察 GetQueuedCompletionStatus 函數(shù)的參數(shù),會發(fā)現(xiàn),這時只能知道一個 IO操作結(jié)束了, 但是究竟是哪個操作結(jié)束了, 或者是哪個SOCKET句柄上的操作結(jié)束了, 并沒有辦法知道。通常這個信息 非常重要,因為只有在IO操作實際完成之后才能釋放發(fā)送或接收等操作的緩沖區(qū)。這些信息可以定義成如下的一個擴(kuò)展OVERLAPPED結(jié)構(gòu):

49、struct MYOVERLAPPEDOVERLAPPED m_ol;intm_iOpType;/ 操作類型 0=AcceptEx 1=DisconnectEx 2=ConnectEx 3=WSARecv 等等SOCKETm_skServer;/服務(wù)端 SOCKETSOCKET m_skClient;/客戶端 SOCKETLPVOIDm_pBuf;本次IO操作的緩沖指針其它需要的信息;使用時:MYOVERLAPPED* pMyOL = new MYOVERLAPPED;ZeroMemory(pMyOL,sizeof(MYOVERLAPPED);pMyOL->m_iOpType = 0;

50、/AcceptEx 操作pMyOL->m_skServer = skServer;pMyOL->m_skClient = skClient;BYTE* pBuf = new BYTE256;一個緩沖/朝緩沖中寫入東西pMyOL->m_pBuf = pBuf;/其它的代碼AcceptEx(skServer, skClient,pBuf,0,/將接收緩沖置為0,令A(yù)cceptEx直接返回256,256,NULL,(LPOVERLAPPED)pMyOL);/注意最后這個強(qiáng)制類型轉(zhuǎn)換在完成過程回調(diào)線程函數(shù)中,這樣使用:UINT CALLBACK Client_IOCPThread(v

51、oid* pParam)/IOCP線程函數(shù)DWORD dwBytesTrans = 0;DWORD dwPerData = 0;LPOVERLAPPED lpOverlapped = NULL;while(1)/又見死循環(huán)呵呵呵BOOL bRet = GetQueuedCompletionStatus( pThis->m_IOCP,&dwBytesTrans,&dwPerData,&lpOverlapped,INFINITE);if( NULL = lpOverlapped )沒有真正的完成SleepEx(20,TRUE);故意置成可警告狀態(tài) continue;/

52、找回火車頭”以及后面的所有東西MYOVERLAPPED* pOL = CONTAINING_RECORD(lpOverlapped ,MYOVERLAPPED, m_ol);switch(pOL->m_iOpType)case 0: /AcceptEx 結(jié)束有鏈接進(jìn)來了 SOCKET句柄就是 pMyOL->m_skClientbreak; /end while/end fun至此,關(guān)于這個 火車頭”如何使用,應(yīng)該是看明白了,其實就是從函數(shù)傳入,又由函數(shù)返回。只不過 其間可能已經(jīng)轉(zhuǎn)換了線程環(huán)境,是不同的線程了。這里再補(bǔ)充一個 AcceptEx容易被遺漏的一個細(xì)節(jié)問題,那就是在Acce

53、ptEx完成返回之后,如下在那個連人的客戶端 SOCKET上調(diào)用一下:int nRet = :setsockopt(pOL->m_skClient,SOL_SOCKET,SO_UPDATE_ACCEPT_CONTEXT,(char *)&pOL->m_skServer,sizeof(SOCKET);這樣才可以繼續(xù)在這個代表客戶端連接的pOL->m_skClient 上繼續(xù)調(diào)用WSARecv和WSASend 。另外,在AcceptEx完成之后,通常可以用:LPSOCKADDR addrHost = NULL;服務(wù)端地址LPSOCKADDR addrClient = NU

54、LL;/ 客戶端地址int lenHost = 0;int lenClient = 0;GetAcceptExSockaddrs(pOL->m_pBuf,0,sizeof(sockaddr_in) + 16,sizeof(sockaddr_in) + 16,(LPSOCKADDR*) &addrHost,&lenHost,(LPSOCKADDR*) &addrClient,&lenClient);這樣來得到連入的客戶端地址,以及連人的服務(wù)端地址,通常這個地址可以和這個客戶端的SOCKET綁定在一起用 map或hash表保存,方便查詢,就不用再調(diào)用那個get

55、peername得到客戶端的地址了。 要注意的是 GetAcceptExSockaddrs 也是一個 WinSock2 擴(kuò)展函數(shù),專門配合 AcceptEx使用的,需要像 AcceptEx那樣動態(tài)載入一下,然后再調(diào)用,詳情請見前一篇文章中的CGRSMsSockFun 類。至此AcceptEx算討論完整了, OVERLAPPED 的派生定義也講完了,讓我們繼續(xù)下一步。四、編寫線程池回調(diào)函數(shù):在討論擴(kuò)展定義 OVERLAPPED 結(jié)構(gòu)體時,給出了非線程池版的線程函數(shù)的大概框架,也就是傳統(tǒng) IOCP使用的自建線程使用方式,這種方式要自己創(chuàng)建完成端口句柄,自己將 SOCKET句柄綁定到完成端口,這里就

56、不在贅述,主要介紹下調(diào)用BindloCompletionCallback 函數(shù)時,應(yīng)如何編寫這個線程池的回調(diào)函數(shù),其實它與前面那個線程函數(shù)是很類似的。先來看看回調(diào)函數(shù)長個什么樣子:VOID CALLBACK FileIOCompletionRoutine(inDWORD dwErrorCode,inDWORD dwNumberOfBytesTransfered,inLPOVERLAPPED lpOverlapped);第一個參數(shù)就是一個錯誤碼,如果是0恭喜你,操作一切ok,如果有錯也不要慌張,前一篇文章中已經(jīng)介紹了如何翻譯和看懂這個錯誤碼。照著做就是了。第二個參數(shù)就是說這次IO操作一共完成了多少字節(jié)的數(shù)據(jù)傳輸任務(wù),這個字段有個特殊含義,如果你發(fā)現(xiàn)一個Recv操作結(jié)束了,并且這個參數(shù)為0,那么就是說,客戶端斷開了連接(注意針對的是TCP方式,整個SOCKET池就是為TCP方式設(shè)計的)。如果這個情況發(fā)生了,在 SOCKET池中就該回收這 個SOCKET句柄。第三個參數(shù)現(xiàn)在不用多說了,立刻就知道怎么用它了。跟剛才調(diào)用 GetQueuedCompletionStatus 函 數(shù)得到的指

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
  • 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負(fù)責(zé)。
  • 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

最新文檔

評論

0/150

提交評論