Die Wait-Funktionen der Windows-API

DynWaitList: ID-basiertes Multiplexing von Windows-Ereignissen

Alex Gimenez

Beispielcode herunterladen

Microsoft Windows bietet mithilfe der WaitForMultipleObjects-Methode und deren Varianten eine Multiplexing-Abruffunktion für zahlreiche Ereignisse. Diese Funktionen sind zwar leistungsstark, erweisen sich jedoch bei dynamischen Ereignislisten als unpraktisch.

Die Schwierigkeit besteht darin, dass Ereignissignale von Indizes in einem Array aus Objekthandles identifiziert werden. Solche Indizes werden verschoben, wenn Ereignisse mitten zum Array hinzugefügt bzw. mitten aus diesem entfernt werden.

Diese Art des Problems kann normalerweise mithilfe eines Containers, das die Handles enthält, durch Einschließen des Arrays sowie durch Durchführen von Einfüge-, Entfernen- und Suchvorgängen im Namen der Client-Anwendungen behoben werden.

In diesem Artikel werden das Design und die Implementierung einer solchen Containerklasse erläutert. Der Container enthält die Ereignishandles, die mit der WaitForMultipleObjects-Methode verwendet werden. Benutzer der Containerklasse verweisen durch eine numerische ID auf einzelne Handles. Diese IDs bleiben während der Lebensdauer des Containers unverändert, auch beim Hinzufügen oder Entfernen von Ereignissen.

Erforschen des Problems

Die Schnittstelle zu WaitForMultipleObjects/MsgWaitForMultipleObjects ist am besten für einfachere Fälle geeignet, für die Folgendes zutrifft:

  • Sie wissen bereits im Voraus, auf wie viele Handles Sie warten müssen.
  • Die Anzahl der Handles, auf die Sie warten, ändert sich nicht im Verlauf der Zeit.

Wenn ein Handle signalisiert wird, erhalten Sie den Index des Handles als Rückgabewert. Dieser Index – die Position innerhalb des Ereignisarrays wird als Eingabe weitergeleitet – ist nicht unbedingt aussagekräftig. Das Ergebnis, das Sie wirklich mit diesen Funktionen erzielen möchten, besteht im signalisierten Handle oder in nicht flüchtigen Informationen, die Sie zu diesem Handle führen.

In Abbildung 1 wird ein Beispiel zur Verdeutlichung des Problems gezeigt. Der Code – Teil einer theoretischen Medienstreaming-Anwendung – wartet auf ein Signal eines Audiogerätes oder des Netzwerks (dieses Beispiel und andere Codebeispiele sind im Codedownload für diesen Artikel zu finden).

Abbildung 1 Medienstreaming-Anwendung wartet auf ein Signal

#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;
}

Die Ausgabe des Ergebnisses WAIT_OBJECT_0 (Index 0) bedeutet, dass das Audiogerät signalisiert wurde. Wenn als Ergebnis „Index 1“ ausgegeben wird, wurde das Netzwerk signalisiert. Was geschieht nun, wenn Sie audioInHandle aufgrund eines von socketHandle ausgelösten Ereignisses schließen müssen? In diesem Fall müssten Sie Index 0 im Handlesarray löschen, wodurch die Indizes größer als 0 verschoben würden. Dies bedeutet, dass der Wert MY_SOCKET_EVENT ein dynamischer und kein konstanter Wert sein muss.

Es gibt natürlich Möglichkeiten, dieses Problem zu umgehen, z. B. können Sie die optionalen Handles am Ende des Arrays belassen oder den Start des Arrays verschieben. Diese Methoden werden jedoch schnell unübersichtlich, wenn Sie weitere Ereignisse und Fehlerbehandlungspfade hinzufügen (Indizes außerhalb des Werts WAIT_ABANDONED_0).

Auf den ersten Blick scheint das Problem darin zu bestehen, dass keine Konstanten zum Identifizieren der Ereignishandles verwendet werden können. Auf den zweiten Blick stellen wir jedoch fest, dass das Hauptproblem mit dieser Schnittstelle darin besteht, dass sie Arrayindizes zum Identifizieren der Ereignishandles verwendet. Indizes haben dann eine unangenehme Doppelfunktion, da sie sowohl die Position des Handles im Speicher darstellen als auch die Tatsache verdeutlichen, dass es sich um ein signalisiertes Ereignis handelt.

Es wäre schön, wenn die signalisierten Ereignisse unabhängig von den Indizes im Array identifiziert werden könnten. Genau dies wird durch die DynWaitList-Klasse erreicht.

Verwenden von DynWaitList

Die DynWaitList-Klasse ist eine Containerliste für das Array der Handles, die für die WaitForMultipleObjects-Methode übergeben werden sollen. Die interne Sammlung der Handles besitzt eine statische Maximalgröße. Die Klasse wird als Vorlage implementiert, wobei die Maximalgröße der Sammlung der einzige Vorlagenparameter ist.

Die Containerschnittstelle bietet genau die Methoden, die Sie erwarten: „Hinzufügen“ zum Einfügen eines Ereignisses und Definieren von dessen ID sowie „Entfernen“ zum Entfernen eines Ereignisses und einiger Varianten der Wait-Funktion. In Abbildung 2 wird die Verwendung von DynWaitList zur Behebung des vorher erläuterten Problems gezeigt.

Abbildung 2 Verwenden von 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;
}

Allgemeine Verwendungszwecke von DynWaitList

Das hier angegebene Beispiel zeigt eine kleine Anzahl von bekannten Ereignis-IDs. Es gibt jedoch auch Fälle, bei denen die IDs zahlreich und nicht vorher bekannt sind. Im Folgenden sind einige allgemeine Fälle angegeben:

  • Ein TCP-Server, der eine Ereignis-ID für jeden zurzeit verbundenen Clientsocket enthält. Hierbei profitieren Sie am besten von der dynamischen Ereignisliste, da Clientsockets mit jeder Verbindung geöffnet und geschlossen werden.
  • Eine Audiomixing- oder IP-Telefon-Anwendung mit einem Handle, das auf das Signal „frame-ready/timer“ des jeweiligen Audiogeräts im System wartet.

Die Beispiele haben bisher eine Gemeinsamkeit: Die dynamische Liste der Handles ist repräsentativ für die sich ändernde externe Anwendungsumgebung.

Entwurfs- und Leistungsüberlegungen

Die Implementierung eines Containers ist ein Balanceakt, bei dem zwischen den Zielkonflikten hinsichtlich Leistung, Einfachheit und Speicherplatz eine Auswahl getroffen wird. Diese müssen im Hinblick der häufigsten Containerverwendungen beurteilt werden – derjenigen, die zuvor erläutert wurden. Es ist auch hilfreich, die für den Container durchgeführten Vorgänge sowie die Häufigkeit ihrer Ausführung anzugeben:

  • Hinzufügen eines Handles: sehr häufig
  • Entfernen eines Handles: ungefähr so häufig wie das Hinzufügen eines Handles
  • Ändern eines Handles: nicht zutreffend (das Handle eines vorhandenen Objekts kann unter Windows nicht geändert werden)
  • Umwandeln des Containers in das flache Array, das Windows benötigt: sehr häufig
  • Abrufen des Werts eines Handles, das soeben signalisiert wurde: sehr häufig

Angesichts dieser Vorgänge entschied ich mich, einen internen Speicher (den von Windows benötigten Speicher) als Array von Ereignishandles zu verwenden plus ein paralleles Array von IDs, bei denen es sich um 16-Bit-Werte handelt. Diese Anordnung von parallelen Arrays ermöglicht eine effiziente Umwandlung zwischen Indizes und Ereignis-IDs. Insbesondere:

  • Das von Windows benötigte Array ist stets verfügbar.
  • Angesichts des von Windows zurückgegebenen Index ist die Suche nach dessen ID ein Order-1-Vorgang.

Als eine weitere wichtige Überlegung ist die Threadsicherheit zu nennen. In Anbetracht des Zwecks dieses Containers ist es fair, dass die Vorgänge serialisiert werden sollen, sodass ich mich entschied, die internen Arrays nicht zu schützen.

In Abbildung 3 wird die Deklaration der Klasse mit der Hauptschnittstelle und den internen Komponenten des Containers gezeigt.

Abbildung 3 Klassendeklaration mit der Hauptschnittstelle und den internen Komponenten des Containers

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 ];
};

Die Klasse besteht aus zwei Teilen: einer Basisklasse, die den Arrayzeiger enthält, und einer vorlagenabgeleiteten Klasse, die den tatsächlichen Speicher enthält. Dies bietet die Flexibilität für eine dynamische Arrayzuweisung, gegebenenfalls durch die Ableitung einer anderen Vorlagenklasse. Diese Implementierung verwendet ausschließlich einen statischen Speicher.

Hinzufügen eines Handles

Das Hinzufügen eines Handles zu einem Array ist sehr einfach, mit Ausnahme der Suche nach einer freien ID für das neu erstellte Ereignis. Gemäß des gewählten Designs ist ein Array von IDs im Container verfügbar. Dieses Array ist so vorbelegt, dass es die maximale Anzahl von IDs enthält, die der Container aufnehmen kann. Aus diesem Grund kann das Array bequem zwei Gruppen von IDs aufnehmen:

  • Die ersten N-Elemente sind die verwendeten IDs, wobei N die Zahl der tatsächlich zugewiesenen Handles darstellt.
  • Bei den verbleibenden Elementen handelt es sich um einen Pool von freien IDs.

Dies erfordert, dass das ID-Array bei der Erstellung mit allen möglichen ID-Werten gefüllt wird. Angesichts dieser Tatsache spielt es keine Rolle, eine freie ID mithilfe der ID genau über der zuletzt verwendeten zu finden. Eine Suche ist nicht erforderlich. Der Code für den Klassenkonstruktor und die Add-Methode wird in Abbildung 4 gezeigt. Diese zwei Methoden wirken zusammen, um den Pool freier IDs zu erstellen und zu verwenden.

Abbildung 4 Klassenkonstruktor und Add-Methode

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;
}

Entfernen eines Handles

Zum Entfernen eines Handles aus dem Container anhand dessen ID müssen wir den Index des Handles finden. Die Umwandlung vom Index zur ID wird durch diese Implementierung auf Order-1 optimiert, die Leistung der Rückumwandlung wird jedoch hierdurch beeinträchtigt. Anhand einer ID erfolgt eine lineare Suche (Order-N) nach dessen Index im Array. Ich entschied mich, diesen Nachteil hinzunehmen, weil sich Benutzer beim Trennen einer Verbindung vom Server nicht unbedingt über die Antwortzeit bewusst sind. Wenn der zu entfernende Index erst einmal gefunden wurde, gestaltet sich der Entfernungsvorgang problemlos. Tauschen Sie einfach das gefundene Handle mit dem zuletzt verwendeten Handle (siehe Abbildung 5).

Abbildung 5 Entfernen eines Handles

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;
  }
}

Erkennen von Signalen

Die Hauptfunktion von DynWaitList besteht in der Erkennung von Signalen. Der Abruf von WaitForMultipleObjects ist unbedeutend, da alle Daten für den Abruf im Archiv aufbewahrt werden. Die Umwandlung des erkannten Signals in eine ID, auf die die übergeordnete Schicht verweisen kann, ist aufgrund des parallelen Arrays von IDs ebenfalls unbedeutend. Dieser Code ist der Hauptbestandteil der Wait-Methode, wie in Abbildung 6 gezeigt. Es gibt einige Varianten von Wait, die alle die interne TranslateWaitResult-Methode zur Umwandlung des Index in die ID nutzen.

Abbildung 6 Erkennen von Signalen

// 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
}

Überlegungen zur Mehrkerntechnologie

Wir nähern uns einer Computingwelt mit mehreren Kernen, in der wir durch Arbeiten in mehreren Threads die Effizienz von Computern nutzen. Ist Multiplexing von Ereignissen in solch einer Computingwelt überhaupt bedeutend? Ist es möglich, dass die meisten Anwendungen ein Ereignis pro Thread haben werden und daher die Vorteile von DynWaitList aufgehoben werden?

Keinesfalls. Ich denke, dass Multiplexing von Ereignissen sogar in Computern mit mehreren Kernen eine große Bedeutung zukommt, zumindest aus den folgenden zwei Gründen:

  • Einige Vorgänge profitieren einfach nicht von der Parallelität, da sie Hardwaregeräte betrifft, auf die seriell zugegriffen werden muss. Low-Level-Networking ist ein solches Beispiel.
  • Ein Hauptvorteil des Multiplexing von Ereignissen besteht – besonders bei Dienstprogramm-Bibliotheken – darin, dass ein bestimmtes Threadmodell keiner Anwendung aufgezwungen wird. Das Threadmodell sollte durch die Anwendung auf der obersten Ebene vorgegeben sein. Auf diese Weise sollte die Anwendung über die Verteilung der Ereignisse in ihren Threads selbst entscheiden können, sodass die Kapselung von Ereigniswartelisten eine noch höhere Bedeutung erhält.

Einfacher Code, weniger Fehler

Zusammenfassend lässt sich sagen, dass die Zuordnung von permanenten IDs zu jedem Ereignis, das an die WaitForMultipleObjects-Funktionen in Windows übergeben wird, zu einfacheren Codes und weniger Fehlern führen kann, da hierdurch das mühevolle Umwandeln von bedeutungslosen Ereignisindizes in nützliche Objekthandles oder -zeiger für die Anwendung beseitigt wird. Die DynWaitList-Klasse kapselt dieses Zuordnungsverfahren effizient in einen Wrapper um diese Wait-Funktionen der Windows-API ein. Alle beteiligten Vorgänge weisen Order-1 auf, mit Ausnahme der Erstellung und Entfernung von Handles, was Order-N hat. Eine weitere Optimierung kann durch Sortieren des Arrays erzielt werden, wodurch das Hinzufügen von Handles etwas länger dauert, das Entfernen von Handles sich jedoch wesentlich schneller ausführen lässt.

Alex Gimenez arbeitet zurzeit als Entwickler in der Nähe von Seattle und schreibt Echtzeitaudio-/-video-/-networking-Software in Zusammenarbeit mit dem für Microsoft Lync verantwortlichen Team. Seine vorherige Erfahrung erstreckt sich über knapp zwei Jahrzehnte im Bereich Point-Of-Sale sowie integrierte, Gerätetreiber- und Telekommunikationssoftware. In seiner Freizeit fährt er gerne Fahrrad, spielt Schlagzeug und lernt Japanisch. Sie erreichen ihn unter alexgim@microsoft.com.

Unser Dank gilt den folgenden technischen Experten für die Durchsicht dieses Artikels: Bart Holmberg, Warren Lam und Jiannan Zheng