完成端口(I/O completion):

異步過程調用(apcs)問題:
    只有發overlapped請求的線程纔可以提供callback函數(需要一個特定的線程爲一個特定的I/O請求服務)。
完成端口(I/O completion)的優點:
    不會限制handle個數,可處理成千上萬個連接。I/O completion port允許一個線程將一個請求暫時保存下來,由另一個線程爲它做實際服務。
併發模型與線程池:
    在典型的併發模型中,服務器爲每一個客戶端創建一個線程,如果很多客戶同時請求,則這些線程都是運行的,那麼CPU就要一個個切換,CPU花費了更多的時間在線程切換,線程確沒得到很多CPU時間。到底應該創建多少個線程比較合適呢,微軟件幫助文檔上講應該是2*CPU個。但理想條件下最好線程不要切換,而又能象線程池一樣,重複利用。I/O完成端口就是使用了線程池。
理解與使用:
第一步:
在我們使用完成端口之前,要調用CreateIoCompletionPort函數先創建完成端口對象。
定義如下:
HANDLE CreateIoCompletionPort(
                                 HANDLE FileHandle,
                                HANDLE ExistingCompletionPort,
                               DWORD CompletionKey,
                               DWORD NumberOfConcurrentThreads
);
FileHandle:
文件或設備的handle, 如果值爲INVALID_HANDLE_VALUE則產生一個沒有和任何文件handle有關係的port.( 可以用來和完成端口聯繫的各種句柄,文件,套接字)
ExistingCompletionPort:
NULL時生成一個新port, 否則handle會加到此port上。
CompletionKey:
用戶自定義數值,被交給服務的線程。GetQueuedCompletionStatus函數時我們可以完全得到我們在此聯繫函數中的完成鍵(申請的內存塊)。在GetQueuedCompletionStatus
中可以完封不動的得到這個內存塊,並且使用它。
NumberOfConcurrentThreads:
參數NumberOfConcurrentThreads用來指定在一個完成端口上可以併發的線程數量。理想的情況是,一個處理器上只運行一個線程,這樣可以避免線程上下文切換的開銷。如果這個參數的值爲0,那就是告訴系統線程數與處理器數相同。我們可以用下面的代碼來創建I/O完成端口。
隱藏在之創建完成端口的祕密:
1. 創建一個完成端口
CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, dwNumberOfConcurrentThreads);
2. 設備列表,完成端口把它同一個或多個設備相關聯。
CreateIoCompletionPort(hDevice, hCompPort, dwCompKey, 0) ;
第二步:
根據處理器個數,創建cpu*2個工作線程:
CreateThread(NULL, 0, ServerWorkerThread, CompletionPort,0, &ThreadID))
與此同時,服務器調用WSASocket,bind, listen, WSAAccept,之後,調用
CreateIoCompletionPort((HANDLE) Accept, CompletionPort... )把一個套接字句柄和一個完成端口綁定到一起。完成端口又同一個或多個設備相關聯着,所以以套接字爲基礎,投遞發送和請求,對I/O處理。接着,可以依賴完成端口,接收有關I/O操作完成情況的通知。再看程序裏:
WSARecv(Accept, &(PerIoData->DataBuf), 1, &RecvBytes, &Flags,
&(PerIoData->Overlapped), NULL)開始調用,這裏象前面講過的一樣,既然是異步I/O,所以WSASend和WSARecv的調用會立即返回。
系統處理:
當一個設備的異步I/O請求完成之後,系統會檢查該設備是否關聯了一個完成端口,如果是,系統就向該完成端口的I/O完成隊列中加入完成的I/O請求列。
然後我們需要從這個完成隊列中,取出調用後的結果(需要通過一個Overlapped結構來接收調用的結果)。怎麼知道這個隊列中已經有處理後的結果呢,調用GetQueuedCompletionStatus函數。
工作線程與完成端口:
和異步過程調用不同(在一個Overlapped I/O完成之後,系統調用該回調函數。OS在有信號狀態下(設備句柄),纔會調用回調函數(可能有很多APCS等待處理了))
GetQueuedCompletionStatus
在工作線程內調用GetQueuedCompletionStatus函數。
GetQueuedCompletionStatus(
    HANDLE CompletionPort,
    LPDWORD lpNumberOfBytesTransferred,
    LPDWORD lpCompletionKey,
    LPOVERLAPPED *lpOverlapped,
    DWORD dwMilliseconds
);
CompletionPort:指出了線程要監視哪一個完成端口。很多服務應用程序只是使用一個I/O完成端口,所有的I/O請求完成以後的通知都將發給該端口。
lpNumberOfBytesTransferred:傳輸的數據字節數
lpCompletionKey:
完成端口的單句柄數據指針,這個指針將可以得到我們在CreateIoCompletionPort中申請那片內存。
lpOverlapped:
重疊I/O請求結構,這個結構同樣是指向我們在重疊請求時所申請的內存塊,同時和lpCompletionKey,一樣我們也可以利用這個內存塊來存儲我們要保存的任意數據。
dwMilliseconds:
等待的最長時間(毫秒),如果超時,lpOverlapped被設爲NULL,函數返回False.
GetQueuedCompletionStatus功能及隱藏的祕密:
GetQueuedCompletionStatus使調用線程掛起,直到指定的端口的I/O完成隊列中出現了一項或直到超時。(I/0完成隊列中出現了記錄)調用GetQueuedCompletionStatus時,調用線程的ID(cpu*2個線程,每個ServerWorkerThread的線程ID)就被放入該等待線程隊列中。
     等待線程隊列很簡單,只是保存了這些線程的ID。完成端口會按照後進先出的原則將一個線程隊列的ID放入到釋放線程列表中。
這樣,I/O完成端口內核對象就知道哪些線程正在等待處理完成的I/O請求。當端口的I/O完成隊列出現一項時,完成端口就喚醒(睡眠狀態中變爲可調度狀態)等待線程隊列中的一個線程。線程將得到完成I/O項中的信息:傳輸的字節數,完成鍵(單句柄數據結構)和Overlapped結構地址,線程是通過GetQueuedCompletionStatus返回這些信息,等待CPU的調度。
GetQueuedCompletionStatus返回可能有多種原因,如果傳遞無效完成端口句柄,函數返回False,GetLastError返回一個錯誤(ERROR_INVALID_HANDLE),如果超時,返回False, GetLastError返回WAIT_TIMEOUT, i/o完成隊列刪除一項,該表項是一個成功完成的I/O請求,則返回True。
    調用GetQueuedCompletionStatus的線程是後進先出的方式喚醒的,比如有4個線程等待,如果有一個I/O,最後一個調用GetQueuedCompletionStatus的線程被喚醒來處理。處理完之後,再調用GetQueuedCompletionStatus進入等待線程隊列中。
深入分析完成端口線程池調度原理:
    假設我們運行在2CPU的機器上。創建完成端口時指定2個併發,創建了4個工作線程加入線程池中等待完成I/O請求,且完成端口隊列(先入先出)中有3個完成I/O的請求的情況:
工作線程運行, 創建了4個工作線程,調用GetQueuedCompletionStatus時,該調用線程就進入了睡眠狀態,假設這個時候,I/O完成隊列出現了三項,調用線程的ID就被放入該等待線程隊列中。
I/O完成端口內核對象(第3個參數等級線程隊列),因此知道哪些線程正在等待處理完成的I/O請求。當端口的I/O完成隊列出現一項時,完成端口就喚醒(睡眠狀態中變爲可調度狀態)等待線程隊列中的一個線程(前面講過等待線程隊列是後進先出)。所以線程D將得到完成I/O項中的信息:傳輸的字節數,完成鍵(單句柄數據結構)和Overlapped結構地址,線程是通過GetQueuedCompletionStatus返回這些信息。
在前面我們指定了併發線程的數目是2,所以I/O完成端口喚醒2個線程,線程D和線程C,另兩個繼續休眠(線程B,線程A),直到線程D處理完了,發現表項裏還有要處理的,就喚醒同一線程繼續處理。
線程併發量:
   併發量限制了與該完成端口相關聯的可運行線程的數目, 它類似閥門的作用。 當與該完成端口相關聯的可運行線程的總數目達到了該併發量,系統就會阻塞任何與該完成端口相關聯的後續線程的執行, 直到與該完成端口相關聯的可運行線程數目下降到小於該併發量爲止。所以解釋了線程池中的運行線程可能會比設置的併發線程多的原因。
    它的作用:
最有效的假想是發生在有完成包在隊列中等待,而沒有等待被滿足,因爲此時完成端口達到了其併發量的極限。此時,一個正在運行中的線程調用 GetQueuedCompletionStatus時,它就會立刻從隊列中取走該完成包。這樣就不存在着環境的切換,因爲該處於運行中的線程就會連續不斷地從隊列中取走完成包,而其他的線程就不能運行了。
注意:如果池中的所有線程都在忙,客戶請求就可能拒絕,所以要適當調整這個參數,獲得最佳性能。
線程併發:D線程掛起,加入暫停線程,醒來後又加入釋放線程隊列。
線程的安全退出:
PostQueudCompletionStatus函數,我們可以用它發送一個自定義的包含了OVERLAPPED成員變量的結構地址,裏面包含一個狀態變量,當狀態變量爲退出標誌時,線程就執行清除動作然後退出。
完成端口使用需要注意的地方:
1.在執行wsasend和wsarecv操作前,請先將overlapped結構體使用memset進行清零。

本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/ventry/archive/2008/10/22/3126194.aspx

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