• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>
            posts - 126,  comments - 73,  trackbacks - 0

            I/O完成端口背后的理論是同時運行的線程數必須有個上界;也就是,500個并發的客戶端請求不必要500個線程存在。那么,合適的并發線程數是多少呢?你會意識到,如果一個機器有兩個CPU,那么在此基礎上多余兩個以上的線程實在是沒有意義。因為,當有超過CPU數量的線程數時,系統不得不耗費時間來進行線程之間的切換,這會浪費寶貴的CPU時鐘周期。
            為每個客戶端創建一個線程還有一個不足,就是創建一個線程雖然比創建一個進程廉價,但它遠遠不是免費的。如果服務器應用程序在啟動時創建一個線程池,并且池子中的線程留駐在程序的整個生命期內,那么服務器的性能會得到提高。I/O完成端口在設計上就是使用了線程池。
            I/O完成端口可能是最復雜的核心對象。要創建一個完成端口你可以使用CreateIoCompletionPort函數。

            HANDLE CreateIoCompletionPort(
            ?? HANDLE??? hfile,
            ?? HANDLE??? hExistingCompPort,
            ?? ULONG_PTR CompKey,
            ?? DWORD???? dwNumberOfConcurrentThreads);
            ???
            或者封裝:
            HANDLE CreateNewCompletionPort(DWORD dwNumberOfConcurrentThreads) {

            ?? return(CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0,
            ????? dwNumberOfConcurrentThreads));
            }

            這個函數根據傳入的參數的不同,可以完成兩個完全不同的功能。一是可以創建一個完成端口,另一個是可以將一個設備與完成端口關聯起來。例如要產生一個完成端口,看起來可以這樣調用函數:
            hCompPort = CreateIoCompletionPort(
            ???INVALID_HANDLE_VALUE,
            ???NULL,
            ???0,
            ???0);

            FileHandle —— 文件或設備的句柄。在產生完成端口時,這個參數應設置為INVALID_HANDLE_VALUE,于是產生一個沒有和任何句柄有關系的port。
            ExistingCompletionPort —— 在產生完成端口時此參數設定為NULL,產生一個新的port。如果此參數指定為一個已存在的完成端口的句柄,那么在上一個參數FileHandle中指定的句柄就會被加到此port上,而不會產生新的port。
            CompletionKey —— 用戶自定義的一個值,將被交給提供服務的線程。此值和FileHandle有關聯。
            NumberOfConcurrentThreads —— 與此完成端口有關聯的線程個數。如果設定為0則為CPU的個數。
            NumberOfConcurrentThreads這個參數是告訴完成端口可同時運行的最大線程數。如果傳0,則默認值為CPU的個數。在大多數情況下為了避免不必要的線程切換你可以傳0。有時你也許會想要增加這個值,那就是當來自客戶端的請求需要一個長時間的計算甚至發生阻塞時,但通常不鼓勵增加這個值。你可以嘗試往這個參數傳不同的值來測試比較服務器的性能。

            關聯一個設備到完成端口
            當你創建完成端口時,系統內核會創建5個不同的數據結構.

            第一個數據結構是設備列表,指明了被關聯到完成端口上的設備。將設備關聯到完成端口上,你同樣是調用CreateIoCompletionPort函數。看起來可以這樣調用函數:
            HANDLE h = CreateIoCompletionPort(hDevice, hCompPort, dwCompKey, 0);

            或者封裝:
            BOOL AssociateDeviceWithCompletionPort(
            ?? HANDLE hCompPort, HANDLE hDevice, DWORD dwCompKey) {

            ?? HANDLE h = CreateIoCompletionPort(hDevice, hCompPort, dwCompKey, 0);
            ?? return(h == hCompPort);
            }
            這樣就添加一個記錄(entry)到一個已經存在的完成端口設備列表上。你在第一個參數上傳遞一個已經存在的完成端口句柄(在第一次調CreateIoCompletionPort時得到),在第二個參數上傳遞設備句柄(這個設備可以是file,socket,
            mailslot,pipe等等),在第三個參數上傳遞一個完成鍵(由用戶定義的值,操作系統不關心你傳遞的是什么值)。每當你關聯一個設備到完成端口上時,系統就添加這些信息到完成端口設備列表上。
            注意:CreateIoCompletionPort很復雜,推薦把它看成兩個函數來使用。下面有個例子把創建端口和關聯放在一次調用中完成,但是不推薦這樣使用。下面的代碼在打開一個文件并在同一時間創建一個新的完成端口,同時關聯該設備與完成端口。所有這個文件I/O請求都將在完成時使用CK_FILE這個完成鍵,完成端口將允許兩個線程來并發執行。
            #define CK_FILE?? 1
            HANDLE hfile = CreateFile(...);
            HANDLE hCompPort = CreateIoCompletionPort(hfile, NULL, CK_FILE, 2);


            第二個數據結構是一I/O完成隊列。當針對一個設備的異步I/O請求完成時,系統就檢查看這個設備在之前是否被關聯到一個完成端口過,如果是,系統就將這個已經完成的I/O請求作為一條記錄,添加到對應的完成端口的I/O完成隊列末尾。在隊列中的每條記錄都指明了4個值:已傳輸的字節數;完成鍵(當設備被關聯到端口上時通過第三個參數設定的);一個指針,指向I/O請求的OVERLAPPED結構;和一個錯誤碼。
            注意:對一個設備發出一個I/O請求,但并不將它記錄到完成端口的完成隊列上是可以的。這不是常態,但有時卻派得上用場。比如,當你通過socket發送數據,但你并不在意這些數據是否被處理時。(或者有時候想用舊有的受激發的event對象機制來激發,而不用I/O完成隊列)
            為了這么做,請你設定一個OVERLAPPED結構,內含一個合法的手動重置(manual-reset)event對象,放在hEvent欄位。然后把該handle的最低位設置為1。雖然這聽起來象是一種黑客行為,但檔案中有交代。看起來象這樣:
            Overlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
            Overlapped.hEvent = (HANDLE) ((DWORD_PTR) Overlapped.hEvent | 1);
            ReadFile(..., &Overlapped);

            同樣,不要忘記在關閉event句柄之前重置低位:
            CloseHandle((HANDLE) ((DWORD_PTR) Overlapped.hEvent & ~1));


            Architecting Around an I/O Completion Port
            當你的服務器應用程序啟動時,它將通過調用像CreateNewCompletionPort這樣的函數來創建I/O完成端口。然后應用程序創建一個線程池來處理client請求。現在你有個問題要問,“線程池中有多少線程?”。這是一個很難回答的問題,我將在“How Many Threads in the Pool?”這個小節中給出詳細的回答。目前,單憑經驗的標準是本地機器上的CPU數再乘以2。比如,一個雙CPU的機器上,你就應該在線程池中創建4個線程。
            池子里的所有線程都將執行相同的線程函數。通常,這個線程函數執行一些初始化工作,然后就進入一個循環,直到服務進程被指示停止時才結束掉。在循環里,線程將自己休眠,等待關聯到完成端口上的設備的I/O請求完成。線程通過調用GetQueuedCompletionStatus函數進入等待。
            下面是函數原型:

            BOOL GetQueuedCompletionStatus(
            ?? HANDLE?????? hCompPort,
            ?? PDWORD?????? pdwNumBytes,
            ?? PULONG_PTR?? CompKey,
            ?? OVERLAPPED** ppOverlapped,
            ?? DWORD??????? dwMilliseconds);
            第一個參數hCompPort傳遞一個完成端口句柄,表明線程監控的是哪一個完成端口。許多服務程序都是使用的單一完成端口,所有I/O請求的完成通知都在這一個端口上。基本上,GetQueuedCompletionStatus的工作就是將調用此函數的線程休眠,直到一條記錄出現在指定完成端口上的I/O完成隊列中,或者直到指定的超時時間到達(超時時間在dwMilliseconds參數中指定)。
            第三個數據結構就是等待中的線程隊列(the waiting thread queue)。在線程池中的每一個調用了GetQueuedCompletionStatus的線程,其線程ID就被放置到等待中的線程隊列里,以使I/O完成端口核心對象能知道當前有哪些線程正等待著處理已經完成了的I/O請求。當有一條記錄出現在端口的I/O完成隊列時,完成端口就喚醒一個在等待隊列中的線程。這個線程會得到組成這條完成記錄的一些信息:已傳輸的字節數,完成鍵,和一個OVERLAPPED結構的地址。這些信息通過傳遞給GetQueuedCompletionStatus函數的三個參數(pdwNumBytes、CompKey、ppOverlapped)返回到線程中。GetQueuedCompletionStatus函數的返回值有點復雜,下面的代碼演示了如何處理不同的返回值:
            DWORD dwNumBytes;
            ULONG_PTR CompKey;
            OVERLAPPED* pOverlapped;

            // hIOCP is initialized somewhere else in the program
            BOOL fOk = GetQueuedCompletionStatus(hIOCP,
            ?? &dwNumBytes, &CompKey, &pOverlapped, 1000);
            DWORD dwError = GetLastError();

            if (fOk) {
            ?? // Process a successfully completed I/O request
            } else {
            ?? if (pOverlapped != NULL) {
            ????? // Process a failed completed I/O request
            ????? // dwError contains the reason for failure
            ?? } else {
            ????? if (dwError == WAIT_TIMEOUT) {
            ???????? // Time-out while waiting for completed I/O entry
            ????? } else {
            ???????? // Bad call to GetQueuedCompletionStatus
            ???????? // dwError contains the reason for the bad call
            ????? }
            ?? }
            }
            如你所預料到的,I/O完成隊列是以隊列的方式移出記錄的,也就是先進先出FIFO。然而,你可能沒有料到的是,調用GetQueuedCompletionStatus函數進入等待的線程卻是以棧的方式來喚醒的,也就是LIFO,最后調用函數進入等待的那個線程被最先喚醒。這樣做的理由是為了進一步提高處理性能。例如,有四個線程在“等待線程隊列”中等待著。如果這個時候只有一條“已完成的I/O”記錄出現了,那么最后一個調用GetQueuedCompletionStatus的線程被喚醒來處理這條記錄。當這個線程處理完這條記錄時,此線程再次調用GetQueuedCompletionStatus函數進入等待線程隊列。那么,此時當有另一條I/O完成記錄出現時,剛才那個線程被繼續喚醒來處理這條新的記錄。(這樣的好處就是可以避免線程間的切換)
            只要I/O請求完成得夠遲緩,那么單個線程就足以處理他們。系統只需要保持喚醒同一個線程,并讓其他三個線程持續休眠。使用LIFO算法,線程就不必花時間將他們在內存中的資源置換到磁盤中(例如棧空間),并從CPU的cache中替換出去。

            I/O完成端口如何管理線程池
            現在該討論為什么完成端口是如此有用了。首先,在你創建完成端口的時候,你通過CreateIoCompletionPort函數的最后一個參數指定可以并行運行的線程數量。我們說過,這個值通常設置為主機上的CPU數量。當已經完成的I/O記錄進入隊列時,完成端口就喚醒等待中的線程。然而,完成端口只能喚醒你所指定的那么多個線程。于是,在雙CPU的機器中,如果有4個I/O請求完成,并有4個線程在等待,但完成端口卻只能喚醒2個線程,另2個線程保持休眠。每個得到喚醒在處理已完成的I/O記錄的線程,處理完成后,又重新調用GetQueuedCompletionStatus函數進入等待。因為線程的喚醒算法是后進先出,所以系統看到還有已完成的I/O記錄在排隊時,又喚醒剛才被喚醒過的線程。
            仔細思考這個過程,你似乎會覺得好象有些問題:既然完成端口僅允許指定數量的線程被喚醒,為什么還要產生多余的線程等待在線程池中呢?比如,在上一段中所敘述的那種情況,在一個雙CPU的主機上,創建一個完成端口,并告之只允許2個線程并行處理。但是創建線程池時我們創建了4個線程(按照2倍CPU的數量),看上去我們好象創建了2個多余的線程——他們永遠也不會被喚醒。(因為LIFO)
            但是不用擔心,完成端口是非常聰明的。當完成端口喚醒一個線程時,會將此線程的ID放入第四個數據結構中——被釋放的線程列表。這可以讓完成端口記住哪些線程是被喚醒了的,并監控這些線程的執行。如果某個在“被釋放的線程列表”中的線程因為調用了某功能而被掛起(即產生阻塞)那么完成端口會偵測到這一情況,并將此線程的ID從“被釋放的線程列表”中移動到“暫停的線程列表”——即第五個數據結構中。
            完成端口的目標就是要保證在“被釋放的線程列表”中的記錄條數保持為你在創建完成端口時所指定的并發線程數。因此,當一個被喚醒的線程因為某種原因而被掛起時,完成端口就會釋放掉另一個在等待中的線程,以次來維持“被釋放的線程列表”中的記錄數保持為你所指定的并發線程數。如果被掛起的線程重新被喚醒,它又會從“暫停的線程列表”中移動到“被釋放的線程列表”。這個時候需要注意的是加上剛才被另外喚醒的一個線程,此時在“被釋放的線程列表”中的記錄數會超過你所指定的并發線程數(看上去很奇怪,但卻是這樣)。完成端口意識到這一情況后將不會再喚醒其他線程,除非并行線程數又降到CPU數量之下。實際并行線程數將只在你指定的最大并行線程數之上停留很短的時間并且線程在循環到再次調用GetQueuedCompletionStatus時這種情況將快速的停止下來。
            注意:一旦線程調用GetQueuedCompletionStatus,那么這個線程就被“分配”到那指定的完成端口上去。系統會假定那些被“分配”的線程代表完成端口在工作。你可以終止線程和完成端口間的這種“分配”關系,通過以下三種方式:
            ??終止線程;
            ??讓線程調用GetQueuedCompletionStatus,并傳遞另一個完成端口的句柄,這樣就終止掉當前的“分配”關系,與另一完成端口產生新的“分配”關系。
            ??銷毀完成端口。

            在線程池中創建多少個線程?
            現在是討論線程池中需要多少個線程的好時機。考慮兩點:首先,當服務程序啟動時你應該創建一個最小的線程集,這樣你就不用象每客戶端每線程模型那樣頻繁地創建和銷毀線程了。因為創建和銷毀線程都需要耗費CPU時間,所以你應該盡可能的少做這些操作。其次,你應該設置線程數的最大值,因為創建太多的線程會相應耗費太多的系統資源。就算大多數資源能被交換出RAM,但你還是應該最小限度地使用系統資源,不要浪費,最好不要產生paging file space。
            你也許應該測試不同的線程數。大多數服務程序(包括MS的IIS)都使用啟發式(heuristic,實際就是枚舉選擇,通過可選擇的方法發現的幾種解決辦法中最合適的一種,在一個程序的連續階段被選擇出來以用于該程序的下一步的一種解決問題的技巧的應用)算法來管理他們的線程池。我推薦你也使用同樣的方法。例如,你可以創建如下的變量來管理線程池。
            LONG g_nThreadsMin;??? // Minimum number of threads in pool
            LONG g_nThreadsMax;??? // Maximum number of threads in pool
            LONG g_nThreadsCrnt;?? // Current number of threads in pool
            LONG g_nThreadsBusy;?? // Number of busy threads in pool

            當你的程序啟動時,你可以創建g_nThreadsMin這么多個線程,都執行相同的線程函數。線程函數看起來是這樣:
            DWORD WINAPI ThreadPoolFunc(PVOID pv) {

            ?? // Thread is entering pool
            ?? InterlockedIncrement(&g_nThreadsCrnt);
            ?? InterlockedIncrement(&g_nThreadsBusy);

            ?? for (BOOL fStayInPool = TRUE; fStayInPool;) {

            ????? // Thread stops executing and waits for something to do
            ????? InterlockedDecrement(&m_nThreadsBusy);
            ????? BOOL fOk = GetQueuedCompletionStatus(...);
            ????? DWORD dwIOError = GetLastError();

            ????? // Thread has something to do, so it's busy
            ????? int nThreadsBusy = InterlockedIncrement(&m_nThreadsBusy);

            ????? // Should we add another thread to the pool?
            ????? if (nThreadsBusy == m_nThreadsCrnt) {??? // All threads are busy
            ???????? if (nThreadsBusy < m_nThreadsMax) {?? // The pool isn't full
            ??????????? if (GetCPUUsage() < 75) {?? // CPU usage is below 75%

            ?????????????? // Add thread to pool
            ?????????????? CloseHandle(chBEGINTHREADEX(...));
            ??????????? }
            ???????? }
            ????? }

            ????? if (!fOk && (dwIOError == WAIT_TIMEOUT)) {?? // Thread timed-out
            ???????? if (!ThreadHasIoPending()) {
            ??????????? // There isn't much for the server to do, and this thread
            ??????????? // can die because it has no outstanding I/O requests
            ??????????? fStayInPool = FALSE;
            ???????? }
            ????? }

            ????? if (fOk || (po != NULL)) {
            ???????? // Thread woke to process something; process it
            ???????? ...

            ???????? if (GetCPUUsage() > 90) {?????? // CPU usage is above 90%
            ??????????? if (!ThreadHasIoPending()) { // No pending I/O requests
            ?????????????? if (g_nThreadsCrnt > g_nThreadsMin)) { // Pool above min

            ????????????????? fStayInPool = FALSE;?? // Remove thread from pool
            ?????????????? }
            ??????????? }
            ???????? }
            ????? }
            ?? }

            ?? // Thread is leaving pool
            ?? InterlockedDecrement(&g_nThreadsBusy);
            ?? InterlockedDecrement(&g_nThreadsCurrent);
            ?? return(0);
            }

            注意:在這章稍早的時候(in the section “Canceling Queued Device I/O Requests”)。我說過:當線程退出的時候,系統自動取消所有已發起但處于掛起中的I/O請求。這就是為什么偽代碼像ThreadHasIoPending這樣的函數是必須的。如果線程還有未完成的I/O請求,就不要允許線程結束。
            許多服務都提供了管理工具,以使管理員可以通過此來控制線程池的行為。例如,設置線程數的最小值和最大值,CPU使用極限,以及最大并發線程數。

            Simulating Completed I/O Requests
            I/O完成端口并不是只能用于device I/O,它也是用來解決線程間通訊問題的卓越的機制。
            BOOL PostQueuedCompletionStatus(
            ?? HANDLE????? hCompPort,
            ?? DWORD?????? dwNumBytes,
            ?? ULONG_PTR?? CompKey,
            ?? OVERLAPPED* pOverlapped);

            這個函數將一個“已完成的I/O”通知添加到一個完成端口的隊列上。第一個參數指定要針對的完成端口,剩下的三個參數,dwNumBtyes,ComKey和pOverlapped指出了線程調用 GetQueuedCompletionStatus時將從對應的返回參數中返回的值。當一個線程從“I/O完成隊列”中取出一條模擬記錄時,GetQueuedCompletionStatus函數將返回TRUE,表明已經成功地執行了一個I/O請求。
            函數PostQueuedCompletionStatus難以置信的有用,它提供了一種方式讓你可以跟線程池里的所有線程通訊。例如,當用戶終止服務應用程序時,你當然希望所有的線程都能夠干干凈凈地退出。但如果有線程等待在完成端口上,并且此時又一直沒有I/O請求到達,那么這些線程就不能被喚醒。通過對線程池中的每個線程調用一次PostQueuedCompletionStatus,每個線程都能得以喚醒,你就可以設計檢查從GetQueuedCompletionStatus的返回參數中得到的值(比如設定完成鍵),可以判斷出是否是程序要終止了,由此可決定是否要做清理工作并退出。
            當使用這項技術,你必須要特別小心。我的例子能工作是因為在池中的線程會死去并不會再次調用GetQueuedCompletionStatus。不管怎樣,如果你想要通知其池中的每個線程一些事情,并且它們會循環到再次調用GetQueuedCompletionStatus,你將會有一個問題。因為線程是按LIFO的順序醒來。所以你的應用程序中你將會使用一些額外的線程同步來確保每個線程獲得了合適的機會來查看它的偽I/O記錄。不使用這個格外的線程同步,一個線程可能會查看相同的通知好幾次。


            ?from:油庫七號
            posted on 2008-10-29 18:36 我風 閱讀(1618) 評論(0)  編輯 收藏 引用
            <2009年12月>
            293012345
            6789101112
            13141516171819
            20212223242526
            272829303112
            3456789

            常用鏈接

            留言簿(12)

            隨筆分類

            隨筆檔案

            文章檔案

            相冊

            收藏夾

            C++

            MyFavorite

            搜索

            •  

            積分與排名

            • 積分 - 326075
            • 排名 - 75

            最新評論

            閱讀排行榜

            評論排行榜

            99久久精品费精品国产| 国产精品美女久久久久久2018| 国产精品久久网| 久久精品无码免费不卡| 伊人久久综合精品无码AV专区| 国产精品久久久久久久久免费 | 久久久久精品国产亚洲AV无码| 狠狠色丁香婷婷久久综合| 久久亚洲AV成人无码国产 | 色婷婷综合久久久久中文一区二区| 久久99精品国产一区二区三区| 99久久国产主播综合精品| 2021国产精品久久精品| 狠狠人妻久久久久久综合| 久久久久久国产精品免费免费| 国产毛片欧美毛片久久久| 精品人妻伦九区久久AAA片69| 久久久精品人妻一区二区三区四 | 亚洲精品tv久久久久久久久久| 日产精品久久久久久久性色| 久久综合视频网站| segui久久国产精品| 久久精品国产第一区二区三区| 一本色道久久88综合日韩精品 | 好久久免费视频高清| 亚洲乱码日产精品a级毛片久久| 久久香蕉国产线看观看乱码| 久久久久久国产精品免费无码| 色99久久久久高潮综合影院 | 久久人妻少妇嫩草AV蜜桃| 久久久网中文字幕| 精品久久久久中文字| 好属妞这里只有精品久久| 无码超乳爆乳中文字幕久久| 久久伊人精品一区二区三区| 武侠古典久久婷婷狼人伊人| 久久影视国产亚洲| 奇米影视7777久久精品人人爽| 一本色综合久久| 久久这里只有精品18| 国产婷婷成人久久Av免费高清|