Compartir a través de


Funciones Wait de la API de Windows

DynWaitList: Multiplexación de eventos de Windows basado en identificador

Alex Gimenez

Descargar el ejemplo de código

Microsoft Windows proporciona escucha multiplexada a varios eventos a través del método WaitForMultipleObjects y sus variantes. Estas funciones son poderosas, pero inconvenientes de usar con listas de eventos dinámicas.

La complejidad es que se identifican las señales de evento por índices en una matriz de controladores de objetos. Tales índices cambian cuando se agregan o eliminan los eventos del medio de la matriz.

Este tipo de problema generalmente se resuelve al tener un contenedor que almacena los controladores, al encapsular la matriz y al realizar la inserción, eliminación y búsquedas en nombre de las aplicaciones cliente.

Este artículo analiza el diseño y la implementación de tal clase contenedora. El contenedor almacena los controladores de evento usados en el método WaitForMultipleObjects. Los usuarios de la clase contenedora hacen referencia a controladores individuales por un identificador numérico que no cambia en el transcurso de la duración del contenedor, incluso si se agregan o eliminan eventos.

Exploración del problema

La interfaz de WaitForMultipleObjects/MsgWaitForMultipleObjects es más adecuada para los casos más sencillos donde:

  • sabe de antemano a cuántos controladores debe esperar.
  • el número de controladores que está esperando no cambia con el pasar del tiempo.

Cuando se señala un controlador, obtiene el índice del controlador como un valor de retorno. Este índice, la posición en la cual se pasa la matriz del evento como entrada, no es significativo directamente. Lo que en verdad quiere de estas funciones es el controlador que se señaló o alguna información que no sea efímera que lo conduzca al mismo.

La figura 1 muestra un ejemplo que ilustra el problema. El código, parte de una aplicación teórica de transmisión de medios, espera una señal de un dispositivo de audio o de la red (encontrará, en el código de descarga de este artículo, este y otros ejemplos de código).

Figura 1 Aplicación de transmisión de medios a la espera de una señal

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

Obtener un resultado de WAIT_OBJECT_0 (índice 0) significa que se señaló al dispositivo de audio. Obtener un índice de 1 significa que se señaló la red. Ahora, ¿qué pasaría si necesita cerrar audioInHandle en respuesta a un evento generado por socketHandle? Tendría que deshacerse del índice 0 en la matriz de controladores, lo que cambiaría a los índices mayores a 0, lo que significa que el valor MY_SOCKET_EVENT necesita ser dinámico y no una constante.

Existen maneras de solucionar este asunto, por supuesto; por ejemplo, puede mantener los controladores opcionales al final de la matriz o desplazar el inicio de la matriz. Sin embargo, todo esto empieza a ponerse caótico al agregar algunos eventos más y en el controlador de las rutas de error (los índices que salen de WAIT_ABANDONED_0).

En un principio, el problema es que no puede usar constantes para identificar los controladores de evento. Al observar más de cerca, podemos ver que el problema de raíz de esta interfaz es que usa índices de matriz para identificar los controladores de evento. Entonces, los índices juegan un papel poco práctico de dos funciones, al representar tanto la posición del controlador en la memoria y el hecho de que el evento está señalado.

Sería agradable que los eventos señalados sean identificables independientemente de los índices de la matriz. Eso es lo que hace por nosotros la clase DynWaitList.

Uso de DynWaitList

La clase DynWaitList es una lista contenedora de la matriz de controladores que se deben pasar por el método WaitForMultipleObjects. La colección interna de controladores tiene un tamaño máximo estático. La clase se implementa como una plantilla, donde el tamaño máximo de la colección es el único parámetro de plantilla.

La interfaz del contenedor tiene los métodos que imaginaría de él: Add para insertar eventos y especificar su identificador, Remove para eliminarlos y unas pocas variantes de Wait. La figura 2 muestra como se usa DynWaitList para solucionar el problema que se presentó antes.

Figura 2 Uso de 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;
}

Usos comunes de DynWaitList

El ejemplo presentado aquí muestra un pequeño número de identificadores de eventos bien conocidos. Sin embargo, existen casos donde hay varios identificadores y no se conoce ninguno de antemano. Estos son algunos casos habituales:

  • Un servidor de TCP, el cual retendría un identificador de evento para cada socket de cliente conectado actualmente. Este es el caso en el que se saca mayor provecho de la lista dinámica de evento, puesto que los sockets de cliente van cambiando según cada conexión.
  • Una aplicación de mezcla de audio o una aplicación de telefonía IP, la cual tendría un controlador para esperar la señal de marco de preparación del temporizador de cada dispositivo de audio del sistema.

Hasta ahora, los ejemplos muestran una trama en común: La lista dinámica de controladores es representativa del entorno externo cambiante que rodea la aplicación.

Reflexiones sobre el diseño y el rendimiento

La implementación de un contenedor implica equilibrar entre metas en oposición de rendimiento, simpleza y espacio de almacenamiento. Éstas se deben evaluar según los usos más frecuentes del contenedor, los que se mostraban anteriormente. Por lo tanto, resulta útil enumerar las operaciones que realizan en el contenedor y su tasa de repetición:

  • Agregar un controlador: bastante frecuente
  • Eliminar un controlador: más o menos la misma frecuencia que agregar un controlador
  • Cambiar un controlador: no aplicable (no se puede cambiar el controlador de un objeto existente en Windows)
  • Traducir el contenedor a la matriz plana que necesita Windows: bastante frecuente
  • Recuperar el valor de un controlador que se acaba de señalar: bastante frecuente

Decidí, con estas operaciones en mente, hacer que el almacenamiento interno sea una matriz de controladores de evento (del tipo que necesita Windows), además de una matriz paralela de identificadores, los cuales son valores de 16 bits. Esta distribución de matriz paralela permite una traducción eficiente entre índices e identificadores de evento. En específico:

  • La matriz que Windows necesita siempre está disponible.
  • Según el índice que devuelve Windows, la búsqueda de su identificador es una operación de orden 1.

Otra reflexión importante es la seguridad de subprocesos. De acuerdo con la finalidad del contenedor, es razonable exigir que las operaciones se serialicen, por lo que escogí no proteger las matrices internas.

La figura 3 muestra la declaración de la clase, donde se puede observar la interfaz principal y el interior del contenedor.

Figura 3 Declaración de clase donde se muestra la interfaz principal y el interior del contenedor

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

Observe cómo la clase se divide en dos, con una clase base que retiene un puntero de matriz y una clase derivada de plantilla que sería lo que retiene el almacenamiento. Esto proporciona flexibilidad para la asignación de matriz dinámica (de ser necesaria) al derivar una clase de plantilla distinta. Esta implementación sólo usa almacenamiento estático.

Agregar un controlador

El añadido de un controlador a la matriz es algo directo, salvo por el acto de encontrar un identificador libre que represente un evento creado recientemente. Existe una matriz de identificadores en el contenedor de acuerdo al diseño escogido. Esta matriz se asigna previamente para retener el número máximo de identificadores posibles por el contenedor. Así, la matriz puede retener de forma práctica dos grupos de contenedores:

  • Los primeros Nelementos son los identificadores en uso, donde N representa los controles que se asignaron.
  • Los elementos restantes son un grupo de identificadores libres.

Esto requiere que la matriz de identificadores se rellene con todos los valores de identificación posibles en la construcción. Dado esto, resulta trivial encontrar un identificador libre al usar el identificador inmediatamente superior al último que se usó. No se necesita una búsqueda. El código para el constructor de clase y el método Add se muestran en la figura 4. Estos dos métodos cooperan para crear y usar el grupo de identificadores libres.

Figura 4 Constructor de clases y método 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;
}

Eliminar un controlador

Para eliminar un controlador del contenedor según su identificador, se debe encontrar el índice del controlador. La traducción de índice a identificador se optimiza a orden 1 con esta implementación, pero trae consigo el contra del rendimiento de la traducción inversa. Con un identificador dado, toma una búsqueda lineal (orden N) para encontrar su índice en la matriz. Decidí asumir ese contra puesto que, en el caso del servidor, los usuarios no tienen en consideración el tiempo de respuesta al desconectarse. Después de encontrar el índice para eliminarlo, la operación de eliminación es sencilla y simple: tan solo cambie el controlador que se encontró en el último controlador “en uso” (vea la figura 5).

Figura 5 La operación de eliminación

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

Detección de señales

La detección de señales es la labor principal de DynWaitList. Resulta trivial llamar a WaitForMultipleObjects, puesto que todos los datos se conservan archivados previamente a la llamada. También resulta trivial traducir la señal detectada a un identificador al que pueda hacer referencia la capa superior, debido a la matriz paralela de identificadores. Ese código es el grueso del método Wait, el cual se muestra en la figura 6. Existen un par de variantes de Wait; todas ellas usan el método TranslateWaitResult para realizar la traducción de índice a identificador.

Figura 6 Detección de señales

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

Reflexiones sobre núcleos múltiples

Estamos marchando hacia un mundo informático de varios núcleos, donde la eficiencia de los equipos depende del trabajo realizado en varios subprocesos. En un mundo de tales características, ¿es realmente importante la multiplexación? ¿Es posible que la mayor parte de las aplicaciones terminen teniendo un evento por subproceso, lo cual neutralizaría los beneficios de DynWaitList?

Sostengo que no será así. Creo que la multiplexación de eventos es importante incluso en equipos de varios núcleos, al menos por dos razones:

  • Algunas operaciones simplemente no se ven beneficiadas del paralelismo, puesto que deben hacer contacto con dispositivos de hardware a los cuales se debe hacer acceso en serie. Un ejemplo de esto son las redes de bajo nivel.
  • Un beneficio clave de la multiplexación de eventos, especialmente para bibliotecas de utilidades, es no insertar un modelo de subprocesos en una aplicación. La aplicación de alto nivel debiese dictar el modelo de subprocesos. De esta forma, la aplicación debiese ser libre de escoger la distribución de eventos en los procesos, lo cual hace que sea aún más importante la encapsulación de listas de espera de eventos.

Código más simple, menos errores

En síntesis, la asociación de identificadores no efímeros a cada evento que se pase a las funciones WaitForMultipleObjects de Windows puede ser conducente a la simplificación del código y la reducción de la posibilidad de tener errores, puesto que elimina la carga que se pone en la aplicación de traducir índices de eventos poco significativos en controladores de objeto o punteros útiles. La clase DynWaitList encapsula eficientemente este proceso de asociación en un contenedor alrededor de las funciones de espera de Windows. Todas las operaciones involucradas son de orden 1, excepto la eliminación de controladores y la construcción, las cuales son de orden N. Se pueden obtener más optimizaciones al organizar la matriz, lo cual agrega una leve dificultad para la adición de controladores pero permite una eliminación mucho más veloz de ellos.

Alex Gimenez trabaja actualmente como desarrollador en los alrededores de Seattle, donde escribe software de audio/vídeo/redes en tiempo real con el equipo responsable de la creación de Microsoft Lync. Su experiencia anterior incluye casi dos décadas de tratar con software de punto de venta, incrustado, controlador de dispositivos y telecomunicaciones. En su tiempo libre, él gusta de andar en bicicleta, tocar la batería y estudiar japonés. Puede ponerse en contacto con él en alexgim@microsoft.com.

Gracias a los siguientes expertos técnicos por su ayuda en la revisión de este artículo: Bart Holmberg, Warren Lam y Jiannan Zheng