本文章是由機器翻譯。

Windows API Wait 函數

DynWaitList:以 ID 為基礎的 Windows 事件多工

賴俊 Gimenez

下載程式碼範例

Microsoft Windows 會提供 multiplexed 聆聽透過 WaitForMultipleObjects 方法與變種的數個事件。 這些函式是功能強大,但若要使用 [事件] 清單是動態時不方便。

困難之處便訊號由該事件索引的物件控制代碼陣列。 若要新增或移除陣列的中間事件時,將會變更這類索引。

這種錯誤通常被解決有一種容器,儲存控點、 換行陣列和用戶端應用程式的身份執行插入、 移除及查閱。

本文將告訴您的設計和實作這種容器類別。 容器會儲存與 WaitForMultipleObjects 方法一起使用的事件控點。 容器類別的使用者,請參閱個別的處理常式的數值識別碼,不會變更容器的存留期中般新增或移除事件。

瀏覽問題

WaitForMultipleObjects/MsgWaitForMultipleObjects 的介面最適合用於較簡單的情況下位置:

  • 您知道事先則要等候多少處理。
  • 您正在等候控制代碼的數目不會變更經過一段時間。

當收到信號控制代碼時,做為傳回值會出現控點的索引。 此索引 — 事件陣列中的位置傳遞做為輸入,不是直接有意義。 您真正需要的這些函式是發出訊號的控制代碼或某些非暫時資訊,可引導您至該控點。

圖 1示範說明問題的範例。 它的程式碼 — 理論媒體資料流處理應用程式的一部份--正在等候信號的音效裝置或從網路 (這和其他程式碼範例可以找到這份文件的程式碼下載中)。

圖 1媒體資料流處理的應用程式等候信號

#define MY_AUDIO_EVENT (WAIT_OBJECT_0)
#define MY_SOCKET_EVENT (WAIT_OBJECT_0 + 1)
HANDLE handles[2];
handles[0] = audioInHandle;
handles[1] = socketHandle;
...
switch( WaitForMultipleObjects(handles) )
{
  case MY_AUDIO_EVENT:
  // Handle audio event 
  break;
  case MY_SOCKET_EVENT:
  // Handle socket event 
  // What happens if we need to stop audio here?
break;
}

取得 WAIT_OBJECT_0 的結果 (索引 0),表示這個音效裝置發出訊號。 取得其索引為 1 表示網路發出訊號。 現在,萬一您需要關閉以回應觸發從 socketHandle 事件 audioInHandle? 您接下來必須擺脫控點陣列時,都會移位大於 0 的索引中的索引 0 — MY_SOCKET_EVENT 值必須是動態的意義,不是常數。

有種方法可以解決這種情況下,當然 (比方說,保留的選擇性的控點結尾的陣列,或移動陣列開頭),但它們似乎有點混亂快速取得當您新增幾個更多的事件和錯誤路徑 (索引鍵的 WAIT_ABANDONED_0) 的處理。

在第一眼中,這個問題是您無法使用常數來識別事件控制代碼。 我們要尋找更接近,看到這個介面的根問題是它會使用陣列索引來識別事件控制代碼。 索引然後播放不方便雙精度浮點數上班這裡,表示在記憶體中的控制代碼的位置和事件收到信號的事實。

如果收到信號的狀態事件已識別獨立從陣列中的索引,它會是好。 這就是 DynWaitList 類別會為我們。

使用 DynWaitList

DynWaitList 類別是要傳遞的 WaitForMultipleObjects 方法的控制代碼陣列的容器清單。 控點的內部集合的靜態的最大大小。 這個類別會實作為範本,其中集合的最大大小是唯一的樣板參數。

容器介面有您所預期的方法:將新增到插入事件,並指定其 ID,移除] 以移除事件和幾個變種的等候。 圖 2示範如何使用 DynWaitList 來解決稍早所呈現的問題。

圖 2使用 DynWaitList

WORD idAudioEvent, idSocketEvent;
DynWaitList<10> handles(100);  // Max 10 events, first ID is 100  

handles.Add( socketHandle,  &idSocketEvent );
handles.Add( audioInHandle, &idAudioEvent  );
...
switch( handles.Wait(2000)  )
{
  case (HANDLE_SIGNALED| idAudioEvent ):
  // Handle audio event
  break;
  case (HANDLE_SIGNALED| idSocketEvent): 
  // Handle socket event
  if( decided_to_drop_audio )
  {
    // Array will shift within; the same ID
    // can be reused later with a different
    // handle if, say, we reopen audio
    handles.Remove(idAudioEvent);

    // Any value outside the 
    // 100...109 range is fine
    idAudioEvent = 0;
  }
  break;

  case (HANDLE_ABANDONED| idSocketEvent):
  case (HANDLE_ABANDONED| idAudioEvent):
  // Critical error paths
  break;

  case WAIT_TIMEOUT:
  break;
}

DynWaitList 的一般用途

此處所提供的範例顯示少數幾個已知的事件識別碼。 其實,Id 其中很多,而不事先已知的情況。 下面是一些常見的情況下:

  • TCP 伺服器,會保留每個目前連線的用戶端通訊端的事件識別碼。 這是會用戶端通訊端不要進出與每次連線的 [動態事件] 清單中,大部分的情況。
  • 音訊混合應用程式或 IP 電話應用程式,必須等待每個音訊裝置的框架集就緒/計時器訊號系統上的控制代碼。

到目前為止的範例會顯示通用佈景主題:控點的動態清單是代表變更外部應用程式周圍環境。

設計和效能考量

容器的實作是選擇的效能、 簡化和儲存空間衝突目標之間的平衡的做法。 這些都必須接受評估的最常見的容器使用光線] 下,先前顯示的。 然後,它可協助列舉在容器和其發生率上執行的作業:

  • 新增一個控點:相當頻繁
  • 移除在控點:關於與新增控點的頻率相同
  • 變更邊控點:不適用 (您不能變更 Windows 中的現有物件的控制代碼)
  • 轉譯成 Windows 需要一般陣列容器:相當頻繁
  • 擷取只收到信號的控制代碼的值:相當頻繁

記住這些作業,我決定有內部儲存區是陣列的事件處理常式 (即所需的 Windows),加上一個平行的識別碼,也就是 16 位元值陣列。 此平行陣列排列方式可讓索引和事件 Id 之間的有效轉譯。 特別是:

  • 陣列,Windows 需要始終是可用的。
  • 指定由 Windows 所傳回的索引,查看它的 ID 是順序 1 的作業。

一個其他重要的考量因素是執行緒安全。 指定此容器的目的,它幾乎可以肯定需要進行序列化,作業,所以我選擇未保護的內部陣列。

圖 3示範顯示主要的介面和容器內部的類別宣告。

圖 3顯示主介面和容器內部的類別宣告

class DynWaitlistImpl
{
  protected: 
    DynWaitlistImpl( WORD nMaxHandles, HANDLE *pHandles, 
      WORD *pIds, WORD wFirstId );

    // Adds a handle to the list; returns TRUE if successful
    BOOL Add( HANDLE hNewHandle, WORD *pwNewId );

    // Removes a handle from the list; returns TRUE if successful
    BOOL Remove( WORD wId );

    DWORD Wait(DWORD dwTimeoutMs, BOOL bWaitAll = FALSE);

    // ...
Some snipped code shown later ...
private:
    HANDLE *m_pHandles;
    WORD *m_pIds;
    WORD m_nHandles;
    WORD m_nMaxHandles;
};

template <int _nMaxHandles> class DynWaitlist: public DynWaitlistImpl
{
  public:
    DynWaitlist(WORD wFirstId): 
    DynWaitlistImpl( _nMaxHandles, handles, ids, wFirstId ) { }
    virtual ~DynWaitlist() { }

  private:
    HANDLE handles[ _nMaxHandles ];
    WORD ids[ _nMaxHandles ];
};

請注意類別中斷兩個,與保留的陣列指標,基底類別和保留實際儲存的範本衍生類別的方式。 這會提供動態陣列配置的彈性,必要時 — 藉由衍生的不同樣板類別。 這個實作就是使用完全靜態的儲存體。

新增一個控點

將控點加入至陣列是簡單,除了的尋找一個可用的識別碼,以表示新建立的事件的動作。 每所選擇的設計,沒有在容器中的 Id 陣列。 這個陣列是預先配置來儲存高達識別碼容器可保留的最大數目。 因此,陣列可以方便地保留兩個群組的識別碼:

  • 第一個n項目是 「 編號 在使用,其中 n是實際配置多少控點。
  • 剩餘的項目是可用識別碼的集區。

這需要要填滿所有可能在建構時的 ID 值的 ID 陣列。 假設輕而易舉的事要尋找免費 ID 使用 ID只是過去使用的最後一個。 沒有搜尋是必要的。 類別建構函式和 Add 方法的程式碼所示圖 4。 若要建置並使用可用識別碼的集區相互合作,這兩種方法。

圖 4類別建構函式,並加入方法

DynWaitlistImpl::DynWaitlistImpl(  
  WORD nMaxHandles,  // Number of handles
  HANDLE *pHandles,   // Pointer to array of handle slots
  WORD *pIds,         // Pointer to array of IDs
  WORD wFirstID)      // Value of first ID to use
// Class Constructor.
Initializes object state
:  m_nMaxHandles(nMaxHandles)
,  m_pHandles(pHandles)
,  m_pIds(pIds)
,  m_nHandles(0)
{
  // Fill the pool of available IDs
  WORD wId = wFirstID;
  for( WORD i = 0; i < nMaxHandles; ++i )
  {
    m_pIds[i] = wId;
    wId++;
  }
}


BOOL DynWaitlistImpl::Add(
  HANDLE hNewHandle, // Handle to be added
  WORD *pwNewId ) // OUT parameter - value of new ID picked
// Adds one element to the array of handles
{
  if( m_nHandles >= m_nMaxHandles )
  {
    // No more room, no can do
    return FALSE;
  }
  m_pHandles[ m_nHandles ] = hNewHandle;

  // Pick the first available ID
  (*pwNewId) = m_pIds[ m_nHandles ];

  ++m_nHandles;
  return TRUE;
}

移除在控點

若要移除指定其 ID 的容器控點,我們必須找到控點的索引。 從索引識別碼來轉譯經過最佳化,可順序 1 透過此實作中,但這會出現在反向轉譯效能的代價。 指定的識別碼,花線性搜尋 (順序-N) 來尋找陣列中的它的索引。 我決定採用該叫用,因為,如果是一個伺服器,使用者並非以表達時中斷連線的回應時間。 尋找要移除的索引,移除作業後快速又簡單 — 只要交換與最後一個 「 使用 」 控點的控點 (請參閱圖 5)。

圖 5移除作業

BOOL DynWaitlistImpl::Remove(
  WORD wId ) // ID of handle being removed
// Removes one element from the array of handles
{
  WORD i;
  BOOL bFoundIt = FALSE;
  for( i = 0; i < m_nHandles; ++i )
  {
    // If we found the one we want to remove
    if( m_pIds[i] == wId )
    {
      // Found it!
bFoundIt = TRUE;
      break;
    }
  }

  // Found the ID we were looking for?
if( bFoundIt )
  {
    WORD wMaxIdx = (m_nHandles - 1);
    if( i < wMaxIdx ) // if it isn't the last item being removed
    {
      // Take what used to be the last item and move it down,
      // so it takes the place of the item that was deleted
      m_pIds    [i] = m_pIds    [ wMaxIdx ];
      m_pHandles[i] = m_pHandles[ wMaxIdx ];

      // Save the ID being removed, so it can be reused in a future Add
      m_pIds    [ wMaxIdx ] = wId;
    }
    --m_nHandles;
    m_pHandles[m_nHandles] = 0;
    return TRUE;
  }
  else
  {
    return FALSE;
  }
}

偵測訊號

偵測訊號是主要的工作執行的 DynWaitList。 呼叫 WaitForMultipleObjects 是一般的因為所有的資料會保留 pre-archived 的呼叫。 轉譯成上層通訊可以參考的 ID 偵測到的訊號也是易如反掌因為的平行的 Id 陣列。 程式碼是最基本的部份等待中顯示的方法 圖 6。 有一些變化形式的等待,其中都使用內部的 TranslateWaitResult 方法來執行索引--識別碼轉譯。

圖 6偵測訊號

// Snippet from the header file – Wait is a quick, inline method
DWORD Wait(DWORD dwTimeoutMs, BOOL bWaitAll = FALSE)
{
  return TranslateWaitResult(
    WaitForMultipleObjects( m_nHandles,
    m_pHandles,
    bWaitAll,
    dwTimeoutMs )
    );
}

// Snippet from the CPP file – method called by all flavors of Wait
DWORD DynWaitlistImpl::TranslateWaitResult(
  DWORD dwWaitResult ) // Value returned by WaitForMultipleObjectsXXX
  // translates the index-based value returned by Windows into
  // an ID-based value for comparison
{

  if( (dwWaitResult >= WAIT_OBJECT_0) && 
    (dwWaitResult < (DWORD)(WAIT_OBJECT_0 + m_nHandles) ) )
  {
    return HANDLE_SIGNALED | m_pIds[dwWaitResult - WAIT_OBJECT_0];
  }
  if( (dwWaitResult >= WAIT_ABANDONED_0) && 
    (dwWaitResult < (DWORD)(WAIT_ABANDONED_0 + m_nHandles) ) )
  {
    return HANDLE_ABANDONED | m_pIds[dwWaitResult - WAIT_ABANDONED_0];
  }

  return dwWaitResult; // No translation needed for other values
}

多核心的考量

我們要 marching 向 「 核心 」 電腦世界中,讓我們擷取效率電腦藉由在多個執行緒中執行工作。多,這種世界中事件工甚至重要吗?它可能是大部分的應用程式將會得到每個執行緒,因此 neutralizing 的 DynWaitList 優點的一個事件結束嗎?

我不敢打賭。我相信事件多工是重要即使是在具有多個核心,電腦最少,因為這兩個原因:

  • 某些作業只是沒有受到平行助益,因為其碰觸到的硬體裝置,必須以序列方式存取。低階網路是其中一個範例。
  • 事件多工的主要好處之一 — 尤其是在公用程式庫 — 並不是要推入特定的執行緒模型在應用程式時。最上層應用程式應該遵守的執行緒模型。如此一來,在應用程式應該是無限制地選擇事件散佈到其執行緒,使事件的封裝等候甚至更為不可或缺的清單。

簡單的程式碼,錯誤更少

加總,建立非暫時識別碼,以傳遞至 Windows WaitForMultipleObjects 函式每個事件的關聯可能會導致程式碼簡化並減少錯誤的可能性,它會移除對應用程式將不具任何意義事件索引轉譯成有用的物件控制代碼或指標的負擔。DynWaitList 類別有效率地封裝此關聯程序到這些 Windows API 等候函式的包裝函式。所有相關的作業有為順序-1,除了建構和控制代碼移除哪些屬於順序 n。可藉由排序陣列,即可加入來處理時進行處理移除快很多加法的些微計罰規則達成進一步最佳化。

Alex Gimenez 目前運作身為開發人員附近 「 西雅圖 」 撰寫即時音訊/視訊/網路軟體與小組負責傳遞 Microsoft Lync。他方面的經驗跨越這幾乎項因應銷售點、 內嵌,裝置驅動程式和電訊軟體。在他閒餘的時間,他喜歡自行車、 drumming 和研究日文。他可以連絡到在alexgim@microsoft.com

多虧了要對下列技術專家,來檢閱這份文件:Bart Holmberg, Warren LamJiannan Zheng