Condividi tramite


Funzioni di attesa dell'API di Windows

DynWaitList: Multiplexing di eventi Windows basati su ID

Alex Gimenez

Scarica il codice di esempio

Microsoft Windows offre funzionalità di ascolto in multiplex per diversi eventi tramite il metodo WaitForMultipleObjects e relative varianti. Tali funzioni sono potenti, ma scomode da utilizzare quando l'elenco di eventi è dinamico.

La difficoltà risiede nel fatto che i segnali degli eventi sono identificati da indici in una matrice di handle di oggetti. Tali indici cambieranno quando vengono aggiunti a o rimossi dall'interno della matrice.

Questo tipo di problema si risolve solitamente mediante un contenitore in cui vengono archiviati gli handle, eseguendo il wrapping della matrice ed effettuando l'inserimento, la rimozione e le ricerche per conto delle applicazioni client.

In questo articolo viene illustrata la progettazione e l'implementazione di questa classe contenitore. Nel contenitore vengono archiviati gli handle di eventi utilizzati con il metodo WaitForMultipleObjects. Gli utenti della classe contenitore fanno riferimento ai singoli handle mediante un ID numerico che non cambierà per l'intera durata del contenitore, anche quando gli eventi vengono aggiunti o rimossi.

Analisi del problema

L'interfaccia di WaitForMultipleObjects/MsgWaitForMultipleObjects si adatta meglio a situazioni più semplici in cui:

  • Si conosce in anticipo il numero di handle per cui rimanere in attesa.
  • Il numero di handle per cui rimanere in attesa non cambia nel tempo.

Quando un handle viene segnalato, si ottiene l'indice dell'handle come valore restituito. Tale indice, ovvero la posizione all'interno della matrice di eventi passata come input, non è immediatamente significativo. Ciò che ci interessa veramente di queste funzioni è l'handle segnalato o alcune informazioni non transitorie riconducibili a tale handle.

Nella Figura 1 viene mostrato un esempio che illustra il problema. Il relativo codice, parte di un'applicazione per lo streaming multimediale teorico, è in attesa di un segnale da parte di un dispositivo audio o dalla rete (questo e altri esempi di codice sono disponibili nel download di codice associato a questo articolo).

Figura 1 Applicazione per lo streaming multimediale in attesa di un segnale

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

La ricezione di un risultato da WAIT_OBJECT_0 (indice 0) significa che il dispositivo audio è stato segnalato. La ricezione di un indice con valore 1 significa che la rete è stata segnalata. Ora, cosa accade se è necessario chiudere l'elemento audioInHandle in conseguenza di un evento attivato dall'elemento socketHandle? Occorrerebbe eliminare l'indice con valore 0 dalla matrice di handle, che comporterebbe un cambiamento degli indici con valore maggiore di 0, il che significa che il valore MY_SOCKET_EVENT deve essere dinamico e non costante.

Esistono diversi modi per risolvere la situazione, naturalmente (ad esempio, mantenendo gli handle facoltativi alla fine della matrice o cambiano l'inizio della matrice), ma si rivelano problematici molto rapidamente quando si aggiungono altri eventi e la gestione dei percorsi di errore (indici di WAIT_ABANDONED_0).

A prima vita, il problema sembra essere dovuto al fatto che non è possibile utilizzare costanti per identificare gli handle di eventi. A un'analisi più approfondita, ci rendiamo conto che il problema principale con l'interfaccia è riconducibile al fatto che vengono utilizzati indici di matrice per identificare gli handle di eventi. Gli indici svolgono, quindi, un doppio ruolo scomodo in questo caso, poiché rappresentano sia la posizione in memoria dell'handle che la segnalazione dell'evento.

Sarebbe interessante se gli eventi segnalati fossero identificabili a prescindere dagli indici nella matrice. Ecco che entra in gioco la classe DynWaitList.

Utilizzo della classe DynWaitList

La classe DynWaitList è un elenco contenitore per la matrice di handle da passare per il metodo WaitForMultipleObjects. La raccolta interna di handle ha una dimensione massima statica. La classe è implementata come modello, in cui la dimensione massima della raccolta rappresenta l'unico parametro del modello.

L'interfaccia del contenitore dispone dei metodi che servono allo scopo: Il metodo Add per inserire un evento e specificarne l'ID, il metodo Remove per rimuovere un evento e alcune varianti del metodo Wait. Nella Figura 2 viene illustrato come viene utilizzata la classe DynWaitList per risolvere il problema presentato in precedenza.

Figura 2 Utilizzo di 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;
}

Usi comuni della classe DynWaitList

Nell'esempio illustrato qui vengono mostrati una ridotta quantità di ID evento più noti. Esistono, tuttavia, casi in cui gli ID sono molti e non sono noti in anticipo, tra cui:

  • Un server TCP che deve conservare un ID evento per ciascun socket client attualmente connesso. Questo è il caso che costituisce la maggior parte dell'elenco di eventi dinamici, poiché i socket client vanno e vengono con ciascuna connessione.
  • Un'applicazione di missaggio audio o un'applicazione di telefonia IP che devono disporre di un handle per attendere il segnale di tipo "frame-ready/timer" di ciascun dispositivo audio nel sistema.

Negli esempi presentati finora troviamo un tema comune: l'elenco dinamico di handle è rappresentativo dell'ambiente esterno all'applicazione in costante mutamento.

Considerazioni sulla progettazione e sulle prestazioni

L'implementazione di un contenitore rappresenta un compromesso tra obiettivi di prestazioni, semplicità e spazio di archiviazione in conflitto. Tali obiettivi vanno valutati alla luce degli usi più frequenti del contenitore, come mostrato in precedenza. Successivamente, torna utile enumerare le operazioni da effettuare sul contenitore e la frequenza delle stesse:

  • Aggiunta di un handle: molto frequente
  • Rimozione di un handle: circa la stessa frequenza con cui si aggiunge un handle
  • Modifica di un handle: non disponibile (non è possibile modificare l'handle di un oggetto esistente in Windows)
  • Conversione del contenitore nella matrice semplice richiesta da Windows: molto frequente
  • Recupero del valore di un handle appena segnalato: molto frequente

Tenendo presenti tali operazioni, ho deciso di utilizzare la risorsa di archiviazione interno come matrice di handle di eventi (quelli richiesti da Windows), oltre a una matrice parallela di ID rappresentati da valori a 16 bit. Questa disposizione basata su una matrice parallela consente una conversione efficiente tra indici e ID evento e nello specifico:

  • La matrice richiesta da Windows è sempre disponibile.
  • Dato l'indice restituito da Windows, la ricerca del relativo ID è un'operazione di tipo "order 1".

Un'altra importante considerazione è rappresentata dalla sicurezza dei thread. Data la finalità di questo contenitore, è lecito richiedere la serializzazione delle operazione, quindi ho scelto di non proteggere le matrici interne.

Nella Figura 3 viene illustrata la dichiarazione della classe in cui viene mostra l'interfaccia principale e i componenti interni del contenitore.

Figura 3 Dichiarazione della classe in cui viene mostrata l'interfaccia principale e i componenti interni del contenitore

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

Si osservi come la classe sia suddivisa in due parti, con una classe di base che contiene un puntatore di matrice e una classe derivata da modello che contiene la risorsa di archiviazione vera e propria. Ciò offre flessibilità per l'allocazione dinamica di matrici, se necessario, mediante la derivazione di una classe di modelli diversi. In questa implementazione viene utilizzata esclusivamente l'archiviazione statica.

Aggiunta di un handle

L'aggiunta di un handle alla matrice è semplice, fatta eccezione per la necessità di dover trovare un ID libero per rappresentare un evento appena creato. In base alla modalità di progettazione prescelta, è disponibile una matrice di ID nel contenitore, il quale viene preallocato per contenere il numero massimo di ID possibili per il contenitore. Pertanto, la matrice può facilmente contenere due gruppi di ID:

  • I primi Nelementi sono gli ID in uso, dove N corrisponde alla quantità di handle effettivamente allocati.
  • Gli elementi rimanenti sono un pool di ID liberi.

Durante la creazione, ciò comporta il riempimento della matrice di ID con tutti i possibili valori ID. Sulla base di ciò, è semplicissimo trovare un ID libero utilizzando l'ID appena successivo all'ultimo utilizzato. Non è necessaria alcuna ricerca. Il codice per il costruttore di classi e il metodo Add è riportato nella Figura 4. Questi due metodi cooperano nella creazione e nell'utilizzo del pool di ID liberi.

Figura 4 Costruttore di classi e metodo Add

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

Rimozione di un handle

Per rimuovere un handle dal contenitore in base al relativo ID, è necessario trovare l'indice dell'handle. La conversione dall'indice all'ID è ottimizzata in base alla modalità "order-1" mediante questa implementazione, ma tutto ciò alle spese delle prestazioni della conversione inversa. Dato un ID, è necessaria una ricerca lineare (order-N) per trovare il relativo indice nella matrice. Ho deciso di orientarmi in tal senso poiché, nel caso di un server, gli utenti non si preoccupano particolarmente dei tempi di risposta durante la disconnessione. Dopo aver trovato l'indice da rimuovere, l'operazione di rimozione è rapida e semplice: è sufficiente scambiare l'handle trovato con l'ultimo handle "in uso" (vedere la Figura 5).

Figura 5 L'operazione di rimozione

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

Rilevamento dei segnali

Il rilevamento dei segnali rappresenta il lavoro principale di DynWaitList. La chiamata al metodo WaitForMultipleObjects perché tutti i dati vengono prearchiviati per la chiamata. Anche la conversione del segnale rilevato in un ID a cui possa fare riferimento il livello superiore è semplicissima grazie alla matrice parallela di ID. Tale codice rappresenta la sostanza del metodo Wait, illustrato nella Figura 6. Esistono alcune varianti del metodo Wait, che utilizzano il metodo TranslateWaitResult per effettuare la conversione da indice a ID.

Figura 6 Rilevamento dei segnali

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

Osservazioni sulle tecnologie multicore

Stiamo passando a un mondo in cui l'elaborazione è basata su processori "a più core", dove sfruttiamo l'efficienza dei computer effettuando operazioni in più thread. In un tale contesto, anche il multiplexing di eventi è importante? Non potrebbe essere che la maggior parte delle applicazioni finisca per disporre di un solo evento per thread, neutralizzando di fatto i vantaggi di DynWaitList?

Non credo proprio. Ritengo che il multiplexing di eventi sia importante anche in computer con più core, almeno per due ragioni:

  • Alcune operazioni semplicemente non traggono vantaggio dal parallelismo, poiché utilizzano dispositivi hardware a cui è necessario accedere in modo seriale. Le funzionalità di rete a basso livello sono un esempio.
  • Un vantaggio principale del multiplexing di eventi, specialmente nelle librerie di utilità, non consiste nell'introdurre un modello di threading particolare in un'applicazione. L'applicazione principale dovrebbe indicare il modello di threading. In tal modo, l'applicazione dovrebbe essere libera di scegliere la distribuzione di eventi nei propri thread, rendendo l'incapsulamento degli elenchi di attesa eventi ancora più importanti.

Codice semplice con meno bug

Riepilogando, l'associazione di ID non transitori a ciascun evento passato alle funzioni del metodo WaitForMultipleObjects di Windows consente una semplificazione del codice e una riduzione delle probabilità del verificarsi di bug, poiché viene rimosso l'onere a carico dell'applicazione di convertire indici di eventi non significativi in handle o puntatori di oggetti utili. La classe DynWaitList incapsula in modo efficiente tale processo di associazione in un wrapper che conterrà le funzioni di attesa dell'API di Windows. Tutte le operazioni interessate sono di tipo "order-1", fatta eccezione per la creazione e la rimozione degli handle che sono operazioni di tipo "order-N". È possibile ottenere un ulteriore livello di ottimizzazione mediante l'ordinamento della matrice, il che comporterebbe una riduzione delle prestazioni delle operazioni di aggiunta di handle ma che velocizzerebbe notevolmente le operazioni di rimozione di handle.

Alex Gimenez attualmente svolge la professione di sviluppatore vicino Seattle e crea software audio/video/di rete in tempo reale con team responsabile di Microsoft Lync. La sua esperienza precedente comprende venti anni di sviluppo di software per dispositivi embedded, driver di dispositivo, di telecomunicazione e POS (Point-Of-Sale). Nel tempo libero, ama andare in bicicletta, suonare i tamburi e studiare il giapponese. È possibile contattarlo all'indirizzo alexgim@microsoft.com.

Un ringraziamento ai seguenti esperti tecnici per la revisione dell'articolo: Bart Holmberg, Warren Lam e Jiannan Zheng