|
http://blog.csdn.net/ithzhang/article/details/8508161轉(zhuǎn)載請注明出處?。?nbsp; IO完成端口
為了將Windows打造成一個出色的服務(wù)器環(huán)境,Microsoft開發(fā)出了IO完成端口。完成端口需要與線程池配合使用。
完成端口背后的理論是并發(fā)運行的線程數(shù)量必須有一個上限。由于太多的線程將會導(dǎo)致系統(tǒng)花費很大的代價在各個線程cpu上下文進行切換。
使用并發(fā)模型與創(chuàng)建進程相比開銷要低很多,但是也需要為每個客戶請求創(chuàng)建一個新的線程。這開銷仍然很大。通過使用線程池可以是性能有很大的提高。IO完成端口需要配合線程池配合使用。
IO完成端口也是一個內(nèi)核對象。調(diào)用以下函數(shù)創(chuàng)建IO完成端口內(nèi)核對象。
[cpp] view plaincopy
這個函數(shù)會完成兩個任務(wù): 一是創(chuàng)建一個IO完成端口對象。
二是將一個設(shè)備與一個IO完成端口關(guān)聯(lián)起來。
hFile就是設(shè)備句柄。
hExistingCompletionPort是與設(shè)備關(guān)聯(lián)的IO完成端口句柄。為NULL時,系統(tǒng)會創(chuàng)建新的完成端口。
dwCompletionKey是一個對我們有意義的值,但是操作系統(tǒng)并不關(guān)心我們傳入的值。一般用它來區(qū)分各個設(shè)備。
dwNumberOfConcurrentThreads告訴IO完成端口在同一時間最多能有多少進程處于可運行狀態(tài)。如果傳入0,那么將使用默認值(并發(fā)的線程數(shù)量等于cpu數(shù)量)。在第二章我們曾介紹說幾乎所有的內(nèi)核對象都需要安全屬性參數(shù)。那時的幾乎就是因為IO完成端口這個例外。它是唯一一個不需要安全屬性的內(nèi)核對象。 這是因為IO完成端口在設(shè)計時就是只在一個進程中使用。
每次調(diào)用CreateIoCompletionPort時,函數(shù)會判斷hExistingCompletionKey是否為NULL,如果為NULL,會創(chuàng)建新的完成端口內(nèi)核對象。并為此完成端口創(chuàng)建設(shè)備列表然后將設(shè)備加入到此完成端口設(shè)備列表中(先入先出)。
設(shè)備列表存儲與該完成端口相關(guān)聯(lián)的所有設(shè)備。
設(shè)備列表只是調(diào)用CreateIoCompletionPort函數(shù)時的一個數(shù)據(jù)結(jié)構(gòu)。除此之外還有四個結(jié)構(gòu)。
第二個結(jié)構(gòu)是IO完成隊列。當(dāng)設(shè)備的一個異步IO請求完成時,系統(tǒng)會檢查該設(shè)備是否與一個完成端口相關(guān)聯(lián),如果關(guān)聯(lián),系統(tǒng)會將這個已完成的IO請求添加到完成端口的IO完成隊列中。每一項包括已傳輸字節(jié)數(shù),完成鍵(dwCompletionKey)值,以及一個指向IO請求的OVERLAPPED結(jié)構(gòu)指針和錯誤碼。
Windows為IO完成端口提供了一個函數(shù),可以將線程切換到睡眠狀態(tài),來等待設(shè)備IO請求完成并進入完成端口。
[cpp] view plaincopy
hCompletionPort表示線程希望對哪個完成端口進行監(jiān)視,GetQueuedCompletionStatus的任務(wù)就是將調(diào)用線程切換到睡眠狀態(tài),也就是阻塞在此函數(shù)上,直到指定的IO完成端口出現(xiàn)一項或者超時。 pdwNumberOfBytesTransferred返回在異步IO完成時傳輸?shù)淖止?jié)數(shù)。 pCompletionKey返回完成鍵。 ppOverlapped返回異步IO開始時傳入的OVERLAPPED結(jié)構(gòu)地址。 dwMillisecond指定等待時間。 函數(shù)執(zhí)行成功則返回true,否則返回false。 第三個結(jié)構(gòu)是等待線程隊列。當(dāng)線程池中的每個線程調(diào)用GetQueuedCompletionStatus時,調(diào)用線程的線程標(biāo)識符會被添加到這個等待線程隊列,這使得IO完成端口對象能知道,有哪些線程當(dāng)前正在等待對已完成的IO請求進行處理。當(dāng)IO完成端口的IO完成隊列中出現(xiàn)一項時,完成端口會喚醒等待線程隊列中的一個線程。這個線程會得到已完成IO項的所有信息:已傳輸字節(jié)數(shù),完成鍵以及OVERLAPPED結(jié)構(gòu)地址。這些信息是通過傳給GetQueuedCompletionStatus的參數(shù)來返回的。
IO完成隊列中的各項是以先入先出方式來進行的。但是喚醒等待隊列中的線程是按照后入先出的方式進行。假設(shè)有四個線程正在等待隊列中等待,如果出現(xiàn)了一個已完成的IO項,那么最后一個由于調(diào)用GetQueuedCompletionStatus而被掛起的線程會被喚醒來處理這一項。當(dāng)處理完該項后,線程會由于再次調(diào)用GetQueuedCompletionStatus而進入等待線程隊列。使用這種算法,系統(tǒng)可以將哪些長時間睡眠的線程換出到磁盤。
作者一直在推崇IO完成端口。接下來我們來討論下為什么IO完成端口這么有用?。。?/span>
前面我們提到過IO完成端口只有配合線程池才能發(fā)揮更大的作用。當(dāng)我們創(chuàng)建并關(guān)聯(lián)設(shè)備時,需要指定有多少個線程并發(fā)運行。一般將這個值設(shè)置為cpu的數(shù)量。當(dāng)已完成的IO項被添加到完成隊列中時,IO完成端口會喚醒正在等待的線程,但是喚醒的線程數(shù)最多不會超過我們指定的數(shù)量。如果有四個IO請求已完成,且有四個線程等待GetQueuedCompletionStatus而被掛起,那么IO完成端口只喚醒兩個線程處理,另外兩個線程繼續(xù)睡眠。此時讀者可能會疑問:既然完成端口只允許喚醒指定數(shù)量的線程,那么為什么還指定更多的線程在線程池中呢?這就涉及到IO完成端口的第四個數(shù)據(jù)結(jié)構(gòu):已釋放線程列表。它存儲已被喚醒的線程句柄。這使得IO完成端口能夠直到哪些線程已經(jīng)被喚醒并監(jiān)視它們的執(zhí)行情況。如果此時已釋放線程由于調(diào)用某些函數(shù)將線程切換到了等待狀態(tài),完成端口會將其從已釋放隊列中移除,并將其添加到已暫停線程列表。
已暫停隊列是IO完成端口第五個數(shù)據(jù)結(jié)構(gòu)。
完成端口的目標(biāo)是根據(jù)創(chuàng)建完成端口時指定的并發(fā)線程數(shù)量,將盡可能多的線程保持在已釋放線程列表中。如果一個已暫停的線程被喚醒,它會離開已暫停線程列表并重新進入已釋放線程列表。這意味著已釋放列表中的線程數(shù)量將大于最大允許的并發(fā)線程數(shù)量。這句話什么意思呢?正在運行的線程數(shù)量加上從暫停線程列表中被釋放的線程數(shù)量使總數(shù)大于最大允許的數(shù)量。這可以使線程數(shù)量在短時間內(nèi)超過指定數(shù)量。
IO完成端口并不一定要用于設(shè)備IO,它還可以進行線程間通信。在可提醒IO中我們介紹了QueueUserAPC。該函數(shù)允許線程將一個APC項添加到另一個線程的隊列中。IO完成端口也存在一個類似的函數(shù): >IO通知追加到IO完成端口?size:14pt"> [cpp] view plaincopy
這個函數(shù)用來將已完成的IO通知追加到IO完成端口的隊列中。
hCompletionPort表示我們要將已完成的IO項添加到哪個完成端口的隊列中。
剩下的三個參數(shù)表示應(yīng)該返回給主調(diào)線程的值。
每個線程都調(diào)用一次GetQueuedCompletionStatus,將它們都喚?E7??程通信。例如:當(dāng)用戶終止服務(wù)程序時,我們想要所有線程退出。如果各個線程還在等待完成端口但有沒有已完成的IO請求,那么無法將它們喚醒。我們可以為線程池中的每個線程都調(diào)用一次GetQueuedCompletionStatus,將它們都喚醒。各線程對GetQueuedCompletionStatus函數(shù)返回值進行檢查,如果發(fā)現(xiàn)應(yīng)用程序正在終止,就會正常退出。(由于線程等待隊列是以棧方式喚醒各線程,為了保證線程池中每個線程都有機會得到模擬IO項,我們還必須在程序中采用其他線程同步機制)
當(dāng)對完成端口調(diào)用CloseHandle時,系統(tǒng)會將所有正在等待GetQueuedCompletionStatus返回的線程喚醒,并返回false。GetLastError返回ERROR_INVALID_HANDLE,此時線程就可以知道應(yīng)該退出了。
下面的例子使用異步IO 實現(xiàn)了文件復(fù)制工作。首先選擇要復(fù)制的文件,并取得源文件大小。點擊復(fù)制按鈕創(chuàng)建一個線程。新線程將完成創(chuàng)建IO完成端口和關(guān)聯(lián)設(shè)備及執(zhí)行文件復(fù)制工作。將源文件和目標(biāo)文件都關(guān)聯(lián)到同一個完成端口,根據(jù)GetQueuedCompletionStatus返回時的完成鍵來區(qū)分到底是屬于誰的異步IO返回。在啟動循環(huán)時采用了一個小伎倆:使用PostQueuedCompletionStatus向IO完成端口發(fā)送一個模擬的異步IO請求。完成鍵設(shè)置為WRITE_KEY。此時程序?qū)?zhí)行從源文件讀數(shù)據(jù)操作。這樣就開動了引擎。直到文件復(fù)制完成。注意源文件和目標(biāo)文件以及GetQueuedCompletionStatus使用的OVERLAPPED結(jié)構(gòu)不要使用同一個。 選擇文件函數(shù): [cpp] view plaincopy
//創(chuàng)建任務(wù)線程: [cpp] view plaincopy
//新線程入口函數(shù): [cpp] view plaincopy
運行結(jié)果: ![]() ![]() ![]() ![]() |
|
|
來自: 求真我 > 《windows核心編程系列白話》