基於 ID 的 Windows 事件多路複用

Microsoft Windows 提供了經過 WaitForMultipleObjects 方法及其變體對多個事件進行多路複用偵聽的功能。 這些函數功能強大,但不便於在動態事件列表中使用。

困難在於事件信號用索引 標識在對象句柄數組中。當在該數組中間添加或刪除事件時,此類索引將變換。數組

一般,此類問題經過使用存儲句柄的容器、包裝數組並表明客戶端應用程序執行插入、刪除和查找來解決。安全

本文將討論此類容器類的設計和實現。容器存儲 WaitForMultipleObjects 方法使用的事件句柄。容器類的用戶經過數字 ID 引用各個句柄,在容器的生存期內,甚至在添加或刪除事件時,該數字 ID 都不會更改。服務器

問題探討

WaitForMultipleObjects/MsgWaitForMultipleObjects 的接口最適合較爲簡單的狀況,這些狀況包括:網絡

  • 您事先了解要等待的句柄數。
  • 所等待的句柄數不會隨時間變化。

當向句柄發送信號時,將得到句柄索引做爲返回值。此索引(做爲輸入傳遞的事件數組中的位置)沒有直接的用途。使用這些函數的實際目的是獲取已發送信號的句柄或致使該句柄的某些非臨時信息。多線程

圖 1 顯示了說明這一問題的示例。它的代碼(理論媒體流應用程序的一部分)等待音頻設備或網絡發送信號(在本文的代碼下載中能夠找到此代碼或其餘代碼示例)。app

圖 1 等待信號的媒體流應用程序socket

   
  
  
  
  
  
  
  
  
  
  1. #define MY_AUDIO_EVENT (WAIT_OBJECT_0)
  2. #define MY_SOCKET_EVENT (WAIT_OBJECT_0 + 1)
  3. HANDLE handles[2];
  4. handles[] = audioInHandle;
  5. handles[1] = socketHandle;
  6. ...
  7. switch( WaitForMultipleObjects(handles) )
  8. {
  9. case MY_AUDIO_EVENT:
  10. // Handle audio event
  11. break;
  12. case MY_SOCKET_EVENT:
  13. // Handle socket event
  14. // What happens if we need to stop audio here?
  15. break;
  16. }

獲得的結果爲 WAIT_OBJECT_0(索引 0)意味着已向音頻設備發送信號。獲得索引 1 意味着已向網絡發送信號。如今,若是須要關閉 audioInHandle 以響應從 socketHandle 觸發的事件,會出現什麼狀況?您將須要刪除句柄數組中的索引 0,這將變換大於 0 的索引,意味着 MY_SOCKET_EVENT 值須要是動態的,而不是常量。ide

雖然有多種解決這種問題的方法(例如,在數組末尾保留可選句柄或變換數組的開頭),但隨着事件增多以及錯誤路徑的處理(索引偏離 WAIT_ABANDONED_0),這些方法將很快變得混亂。函數

乍一看,問題是您不能使用常量標識事件句柄。但經過仔細觀察,咱們發現此接口的根本問題在於它使用數組索引來標識事件句柄。這樣,索引在此處承擔着雙重任務:既表示句柄在內存中的位置又表示已向事件發送信號的事實,這形成了不便。工具

理想的狀況是,被髮送信號的事件可從數組中的索引獨立標識。這正是 DynWaitList 類的用武之處。

使用 DynWaitList

DynWaitList 類是要爲 WaitForMultipleObjects 方法傳遞的句柄數組的容器列表。句柄的內部集合具備靜態最大大小。類做爲模板實現,而集合的最大大小是惟一的模板參數。

容器接口包含所需的方法:用於插入事件並指定其 ID 的 Add 方法,用於刪除事件以及一些 Wait 變體的 Remove 方法。圖 2 顯示瞭如何使用 DynWaitList 解決前文列舉的問題。

圖 2 使用 DynWaitList

   
  
  
  
  
  
  
  
  
  
  1. WORD idAudioEvent, idSocketEvent;
  2. DynWaitList<10> handles(100); // Max 10 events, first ID is 100
  3. handles.Add( socketHandle, &idSocketEvent );
  4. handles.Add( audioInHandle, &idAudioEvent );
  5. ...
  6. switch( handles.Wait(2000) )
  7. {
  8. case (HANDLE_SIGNALED| idAudioEvent ):
  9. // Handle audio event
  10. break;
  11. case (HANDLE_SIGNALED| idSocketEvent):
  12. // Handle socket event
  13. if( decided_to_drop_audio )
  14. {
  15. // Array will shift within; the same ID
  16. // can be reused later with a different
  17. // handle if, say, we reopen audio
  18. handles.Remove(idAudioEvent);
  19. // Any value outside the
  20. // 100...109 range is fine
  21. idAudioEvent = ;
  22. }
  23. break;
  24. case (HANDLE_ABANDONED| idSocketEvent):
  25. case (HANDLE_ABANDONED| idAudioEvent):
  26. // Critical error paths
  27. break;
  28. case WAIT_TIMEOUT:
  29. break;
  30. }

DynWaitList 的常見用法

此處列舉的示例顯示了一些衆所周知的事件 ID。不過,也存在 ID 不少、事先不熟悉的情形。下面是一些常見的情形:

  • TCP 服務器,它存放當前鏈接的每一個客戶端套接字的事件 ID。因爲客戶端套接字隨各個鏈接切換,這種情形最能充分利用動態事件列表。
  • 混音應用程序或 IP 電話應用程序,其中包含等待系統上每一個音頻設備幀就緒/計時器信號的句柄。

至此,這些示例有一個共同的主題:動態句柄列表表明應用程序所處的不斷變化的外部環境。

設計和性能方面的注意事項

實現容器須要平衡選取性能、簡單性以及存儲空間這些衝突的目標。這些須要根據最多見的容器使用狀況(如前文所述)進行評估。這有助於枚舉要在容器上執行的操做以及這些操做的出現頻率:

  • 添加句柄:頻繁
  • 刪除句柄:與添加句柄的頻率大體相同
  • 更改句柄:不適用(沒法在 Windows 中更改現有對象的句柄)
  • 將容器轉換爲 Windows 須要的單根數組:頻繁
  • 檢索剛發送信號的句柄的值:頻繁

鑑於這些操做,我決定將內部存儲設置爲一個事件句柄數組(Windows 須要的那種)以及一個並行的 ID(16 位值)數組。經過這種並行數組安排能夠在索引和事件 ID 之間進行有效的轉換。具體來講:

  • Windows 須要的數組始終可用。
  • 在給定 Windows 返回索引的狀況下,查找其 ID 是 1 階操做。

另外一個重要的注意事項是線程安全。在給定此容器用途的狀況下,應當要求將操做序列化,所以,我選擇不保護內部數組。

圖 3 顯示了對代表主接口和容器內部項的類的聲明。

圖 3 顯示主接口和容器內部項的類聲明

   
  
  
  
  
  
  
  
  
  
  1. class DynWaitlistImpl
  2. {
  3. protected:
  4. DynWaitlistImpl( WORD nMaxHandles, HANDLE *pHandles,
  5. WORD *pIds, WORD wFirstId );
  6. // Adds a handle to the list; returns TRUE if successful
  7. BOOL Add( HANDLE hNewHandle, WORD *pwNewId );
  8. // Removes a handle from the list; returns TRUE if successful
  9. BOOL Remove( WORD wId );
  10. DWORD Wait(DWORD dwTimeoutMs, BOOL bWaitAll = FALSE);
  11. // ...
  12. Some snipped code shown later ...
  13. private:
  14. HANDLE *m_pHandles;
  15. WORD *m_pIds;
  16. WORD m_nHandles;
  17. WORD m_nMaxHandles;
  18. };
  19. template <int _nMaxHandles> class DynWaitlist: public DynWaitlistImpl
  20. {
  21. public:
  22. DynWaitlist(WORD wFirstId):
  23. DynWaitlistImpl( _nMaxHandles, handles, ids, wFirstId ) { }
  24. virtual ~DynWaitlist() { }
  25. private:
  26. HANDLE handles[ _nMaxHandles ];
  27. WORD ids[ _nMaxHandles ];
  28. };

請注意該類是如何拆分爲兩個類的,其中一個基類存放數組指針,一個模板派生類存放實際存儲。這樣,經過派生不一樣的模板類,能夠靈活進行動態數組分配(若是須要)。這種實現徹底使用靜態存儲。

添加句柄

將句柄添加到數組十分簡單,但查找空閒 ID 來表示新建事件的操做除外。根據所選設計,容器中將存在一個 ID 數組。此數組是預先分配的,用於支持容器能夠存放的最大 ID 數。所以,數組能夠方便地存放兩組 ID:

  • N 個元素是正在使用的 ID,其中 N 表示實際分配的句柄數。
  • 剩餘的元素構成一個空閒 ID 池。

這要求在構造時使用全部可能的 ID 值填充 ID 數組。在這種狀況下,使用緊跟 上一個已使用 ID 的 ID 來查找空閒 ID 十分簡單。不須要搜索。圖 4 中顯示了類構造函數和 Add 方法的代碼。這兩種方法共同構建並使用空閒 ID 池。

圖 4 類構造函數和 Add 方法

   
  
  
  
  
  
  
  
  
  
  1. DynWaitlistImpl::DynWaitlistImpl(
  2. WORD nMaxHandles, // Number of handles
  3. HANDLE *pHandles, // Pointer to array of handle slots
  4. WORD *pIds, // Pointer to array of IDs
  5. WORD wFirstID) // Value of first ID to use
  6. // Class Constructor.
  7. Initializes object state
  8. : m_nMaxHandles(nMaxHandles)
  9. , m_pHandles(pHandles)
  10. , m_pIds(pIds)
  11. , m_nHandles()
  12. {
  13. // Fill the pool of available IDs
  14. WORD wId = wFirstID;
  15. for( WORD i = ; i < nMaxHandles; ++i )
  16. {
  17. m_pIds[i] = wId;
  18. wId++;
  19. }
  20. }
  21. BOOL DynWaitlistImpl::Add(
  22. HANDLE hNewHandle, // Handle to be added
  23. WORD *pwNewId ) // OUT parameter - value of new ID picked
  24. // Adds one element to the array of handles
  25. {
  26. if( m_nHandles >= m_nMaxHandles )
  27. {
  28. // No more room, no can do
  29. return FALSE;
  30. }
  31. m_pHandles[ m_nHandles ] = hNewHandle;
  32. // Pick the first available ID
  33. (*pwNewId) = m_pIds[ m_nHandles ];
  34. ++m_nHandles;
  35. return TRUE;
  36. }

刪除句柄

要將句柄從賦予其 ID 的容器中刪除,須要查找該句柄的索引。經過此實現,能夠將索引到 ID 的轉換優化爲 1 階,但這會下降轉換的性能。在給定 ID 的狀況下,須要執行線性搜索(N 階)在數組中查找其索引。對於服務器,相對於斷開鏈接的狀況,用戶不太介意響應時間,所以,我決定接受性能下降的情形。找到要刪除的索引後,刪除操做就變得輕鬆快捷了,只須要交換找到的句柄與上一個「正在使用的」句柄就好了(參見圖 5)。

圖 5 刪除操做

   
  
  
  
  
  
  
  
  
  
  1. BOOL DynWaitlistImpl::Remove(
  2. WORD wId ) // ID of handle being removed
  3. // Removes one element from the array of handles
  4. {
  5. WORD i;
  6. BOOL bFoundIt = FALSE;
  7. for( i = ; i < m_nHandles; ++i )
  8. {
  9. // If we found the one we want to remove
  10. if( m_pIds[i] == wId )
  11. {
  12. // Found it!
  13. bFoundIt = TRUE;
  14. break;
  15. }
  16. }
  17. // Found the ID we were looking for?
  18. if( bFoundIt )
  19. {
  20. WORD wMaxIdx = (m_nHandles - 1);
  21. if( i < wMaxIdx ) // if it isn't the last item being removed
  22. {
  23. // Take what used to be the last item and move it down,
  24. // so it takes the place of the item that was deleted
  25. m_pIds [i] = m_pIds [ wMaxIdx ];
  26. m_pHandles[i] = m_pHandles[ wMaxIdx ];
  27. // Save the ID being removed, so it can be reused in a future Add
  28. m_pIds [ wMaxIdx ] = wId;
  29. }
  30. --m_nHandles;
  31. m_pHandles[m_nHandles] = ;
  32. return TRUE;
  33. }
  34. else
  35. {
  36. return FALSE;
  37. }
  38. }

檢測信號

檢測信號是 DynWaitList 執行的主要任務。調用 WaitForMultipleObjects 十分簡單,由於全部數據已預先歸檔供調用時使用。因爲並行的 ID 數組,將檢測到的信號轉換爲上層能夠引用的 ID 也十分簡單。Wait 方法的主要內容是代碼,如圖 6 所示。Wait 有一些變體,全部變體都使用內部 TranslateWaitResult 方法執行索引到 ID 的轉換。

圖 6 檢測信號

   
  
  
  
  
  
  
  
  
  
  1. // Snippet from the header file – Wait is a quick, inline method
  2. DWORD Wait(DWORD dwTimeoutMs, BOOL bWaitAll = FALSE)
  3. {
  4. return TranslateWaitResult(
  5. WaitForMultipleObjects( m_nHandles,
  6. m_pHandles,
  7. bWaitAll,
  8. dwTimeoutMs )
  9. );
  10. }
  11. // Snippet from the CPP file – method called by all flavors of Wait
  12. DWORD DynWaitlistImpl::TranslateWaitResult(
  13. DWORD dwWaitResult ) // Value returned by WaitForMultipleObjectsXXX
  14. // translates the index-based value returned by Windows into
  15. // an ID-based value for comparison
  16. {
  17. if( (dwWaitResult >= WAIT_OBJECT_0) &&
  18. (dwWaitResult < (DWORD)(WAIT_OBJECT_0 + m_nHandles) ) )
  19. {
  20. return HANDLE_SIGNALED | m_pIds[dwWaitResult - WAIT_OBJECT_0];
  21. }
  22. if( (dwWaitResult >= WAIT_ABANDONED_0) &&
  23. (dwWaitResult < (DWORD)(WAIT_ABANDONED_0 + m_nHandles) ) )
  24. {
  25. return HANDLE_ABANDONED | m_pIds[dwWaitResult - WAIT_ABANDONED_0];
  26. }
  27. return dwWaitResult; // No translation needed for other values
  28. }

多核注意事項

咱們正在跨入「多核」計算世界,咱們經過多線程執行工做,從而提升效率。在這個世界中,事件多路複用是否會更重要呢?大多數應用程序會最終以每一個線程一個事件的方式運行,從而抵消 DynWaitList 的優勢嗎?

我想是不會的。我相信,即便對於使用多核的計算機,事件多路複用也是重要的,其緣由至少有以下兩點:

  • 有些操做面對的硬件設備必須串行訪問,所以,這些操做沒法從並行受益。另外,低層聯網也是一個例子。
  • 事件多路複用的一個重要優勢(尤爲是在實用工具庫中)是不會將特定的線程模型推送到應用程序上。頂層應用程序應規定線程模型。在這種方式下,應用程序應自由選擇 將事件分發到其線程,這使事件等待列表的封裝更加劇要。

代碼簡單,錯誤更少

總之,將非臨時 ID 與傳遞到 Windows WaitForMultipleObjects 函數的每一個事件關聯能夠簡化代碼並下降錯誤的可能性,緣由是它減輕了應用程序將無心義的事件索引轉換爲有用對象句柄或指針的負擔。DynWaitList 類將此關聯進程有效地封裝在這些 Windows API 等待函數的包裝內。涉及的全部操做都屬於 1 階,但構造和句柄刪除除外,它們屬於 N 階。經過對數組排序能夠進行進一步優化,這在提升句柄刪除速度的同時,會輕微下降句柄添加操做的性能。

相關文章
相關標籤/搜索