Funções Wait da API do Windows

DynWaitList: Multiplexação de eventos do Windows baseada em ID

Alex Gimenez

Baixar o código de exemplo

O Microsoft Windows fornece escuta multiplexada para vários eventos por meio do método WaitForMultipleObjects e suas variantes. Essas funções são poderosas, mas inconvenientes para uso quando a lista de eventos for dinâmica.

A dificuldade é que os sinais de eventos são identificados por índices em uma matriz de identificadores de objetos. Esses índices mudarão quando eventos forem adicionados ou removidos do meio da matriz.

Esse tipo de problema é geralmente resolvido quando há um contêiner para armazenar os identificadores, envolvendo a matriz e executando a inserção, a remoção e as consultas em nome dos aplicativos cliente.

Este artigo discute o design e a implementação dessa classe de contêiner. O contêiner armazena os identificadores de eventos usados com o método WaitForMultipleObjects. Os usuários da classe de contêiner referem-se aos identificadores individuais por uma ID numérica que não será alterada durante toda a vida útil do contêiner, mesmo que eventos sejam adicionados ou removidos.

Explorando o problema

A interface para WaitForMultipleObjects/MsgWaitForMultipleObjects é mais adequada para casos mais simples em que:

  • Você sabe de antemão quantos identificadores terá de aguardar.
  • O número de identificadores que você está aguardando não muda com o tempo.

Quando um identificador é sinalizado, você obtém o índice do identificador como um valor de retorno. Esse índice, ou seja, a posição dentro da matriz de eventos passada como entrada, não é significativo diretamente. O que você realmente deseja dessas funções é o identificador que foi sinalizado ou alguma informação não efêmera que o conduza a esse identificador.

A Figura 1 mostra um exemplo que ilustra o problema. Seu código, ou seja, parte de um aplicativo de streaming de mídia teórico, está aguardando por um sinal de um dispositivo de áudio ou da rede (esse e outros códigos de exemplo que podem ser encontrados ao baixar o código deste artigo).

Figura 1 Aplicativo de streaming de mídia aguardando por um sinal

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

Obter um resultado de WAIT_OBJECT_0 (índice 0) significa que o dispositivo de áudio foi sinalizado. Obter um índice 1 significa que a rede foi sinalizada. Agora, o que acontece se você precisar fechar o audioInHandle em resposta a um evento acionado a partir de socketHandle? Você teria de eliminar então o índice 0 da matriz de identificadores, o que mudaria os índices maiores do que 0, significando que o valor de MY_SOCKET_EVENT precisaria ser dinâmico e não uma constante.

Há várias maneiras de contornar essa situação, é claro (por exemplo, mantenha os identificadores opcionais no final da matriz ou mude o início da matriz), mas eles ficam confusos assim que você adiciona algum outro evento e a identificação de caminhos de erro (índices desativados de WAIT_ABANDONED_0).

À primeira vista, o problema é que você não pode usar constantes para identificar os identificadores de eventos. Com uma análise mais cuidadosa, veremos que o problema raiz com essa interface é que ela usa índices de matriz para identificar os identificadores de eventos. Os índices podem desempenhar aqui um inconveniente trabalho dobrado, representando a posição do identificador na memória e o fato de que o evento é sinalizado.

Seria bom se os eventos sinalizados fossem identificáveis de forma independente dos índices na matriz. É o que faz a classe DynWaitList a seguir.

Usando a DynWaitList

A classe DynWaitList é uma lista de contêineres para a matriz de identificadores que serão passados para o método WaitForMultipleObjects. A coleção interna de identificadores possui um tamanho máximo estático. A classe é implementada como um modelo, em que o tamanho máximo da coleção é o único parâmetro do modelo.

A interface do contêiner possui os métodos esperados: Add para inserir um evento e especificar sua ID, Remove para remover um evento e algumas variantes de Wait. A Figura 2 mostra como a DynWaitList é usada para solucionar o problema apresentado anteriormente.

Figura 2 Usando a 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 comuns da DynWaitList

O exemplo apresentado aqui mostra um pequeno número de IDs de eventos conhecidas. Há, entretanto, casos em que as IDs são muitas e não são previamente conhecidas. Estes são alguns casos comuns:

  • Um servidor TCP, que mantém uma ID de evento para cada soquete cliente atualmente conectado. Esse é o caso que faz com que a maioria da lista de eventos dinâmicos, como soquetes cliente, fique instável a cada conexão.
  • Um aplicativo de mixagem de áudio ou um aplicativo de telefone IP, que teria um identificador para aguardar pelo sinal de pronto/cronômetro para quadro de cada dispositivo de áudio no sistema.

Os exemplos mostram até agora um tema comum: a lista dinâmica de identificadores é representativa da mudança do ambiente externo ao redor do aplicativo.

Considerações sobre design e desempenho

A implementação de um contêiner é um exercício de equilíbrio de escolha entre os objetivos conflitantes de desempenho, simplicidade e espaço de armazenamento. Eles devem ser avaliados sob o ponto de vista dos usos mais comuns do contêiner, aqueles mostrados anteriormente. Em seguida, ela ajuda a enumerar as operações a serem executadas no contêiner e sua taxa de ocorrência:

  • Adicionar um identificador: muito frequente
  • Remover um identificador: quase a mesma frequência de adicionar um identificador
  • Alterar um identificador: não aplicável (não é possível alterar o identificador de um objeto existente no Windows)
  • Converter o contêiner na matriz simples que o Windows necessita: muito frequente
  • Recuperar o valor de um identificador que acabou de ser sinalizado: muito frequente

Com essas operações em mente, decidi que o armazenamento interno seria uma matriz de identificadores de eventos (aquela exigida pelo Windows), além de uma matriz paralela de IDs, que são valores de 16 bits. Essa organização de matriz paralela permite a conversão eficiente entre índices e IDs de eventos. Especificamente:

  • A matriz que o Windows precisa está sempre disponível.
  • Em função do índice retornado pelo Windows, a consulta à ID é uma operação de ordem 1.

Outra consideração importante é a segurança do thread. Em função da finalidade deste contêiner, é justo exigir que as operações sejam serializadas, de modo que optei por não proteger as matrizes internas.

A Figura 3 mostra a declaração da classe mostrando a interface principal e a parte interna do contêiner.

Figura 3 Declaração da classe mostrando a interface principal e a parte interna do contêiner

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 como a classe é dividida em duas, com uma classe base mantendo um ponteiro de matriz e uma classe derivada do modelo mantendo o armazenamento real. Isso fornece flexibilidade para alocação de matriz dinâmica, se necessário, derivando uma classe de modelo diferente. Essa implementação usa o armazenamento estático exclusivamente.

Adicionando um identificador

Adicionar um identificador a uma matriz é muito simples, exceto pelo ato de localizar uma ID livre para representar um evento recém-criado. Dependendo do design escolhido, há uma matriz de IDs no contêiner. Essa matriz é pré-alocada para suportar o número máximo de IDs que o contêiner pode manter. Além disso, a matriz pode manter de forma conveniente dois grupos de IDs:

  • Os N primeiros elementos são as IDs em uso, em que N significa quantos identificadores são realmente alocados.
  • Os elementos restantes são um conjunto de IDs livres.

Isso exige que a matriz de ID seja preenchida com todos os valores de ID possíveis no momento da construção. Por essa razão, é simples localizar uma ID livre usando a ID que segue imediatamente a última que foi usada. Não é necessária uma pesquisa. O código do construtor da classe e o método Add são mostrados na Figura 4. Esses dois métodos contribuem na construção e no uso do conjunto de IDs livres.

Figura 4 Construtor de classe e 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;
}

Removendo um identificador

Para remover um identificador do contêiner em função de sua ID, é necessário localizar o índice do identificador. A conversão do índice em ID é otimizada para a ordem 1 por essa implementação, mas isso ocorre às custas do desempenho da conversão reversa. Quando uma ID é especificada, ocorre uma pesquisa linear (ordem N) para localizar seu índice na matriz. Decidi fazer essa opção porque, no caso de um servidor, os usuários não ficam tão atentos ao tempo de resposta ao desconectar. Depois de localizar o índice a ser removido, a operação de remoção é rápida e simples — basta trocar o identificador encontrado pelo último identificador “em uso” (consulte a Figura 5).

Figura 5 Operação de remoção

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

Detectando sinais

A detecção de sinais é o principal trabalho executado pela DynWaitList. Chamar WaitForMultipleObjects é simples, pois todos os dados são mantidos pré-arquivados para a chamada. Converter o sinal detectado em uma ID à qual a camada superior pode fazer referência também é simples devido à matriz paralela de IDs. Esse código é a parte mais importante do método Wait, mostrado na Figura 6. Há poucas variações de Wait e todas usam o método interno TranslateWaitResult para a conversão de índice em ID.

Figura 6 Detectando sinais

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

Considerações para vários núcleos

Estamos caminhando para um mundo computacional “de vários núcleos”, no qual extraímos a eficiência dos computadores executando o trabalho em vários threads. Nesse mundo, a multiplexação de eventos é mesmo importante? Será que a maioria dos aplicativos acabará tendo um evento por thread, além de neutralizar os benefícios da DynWaitList?

Aposto que não. Acredito que a multiplexação de eventos seja importante nos computadores com vários núcleos, pelo menos por estas duas razões:

  • Algumas operações simplesmente não se beneficiam do paralelismo, pois elas tocam nos dispositivos de hardware que devem ser acessados em modo serial. Os sistemas de rede de baixo nível são um exemplo.
  • Um benefício importante da multiplexação de eventos, especialmente em bibliotecas de utilitários, é não forçar um determinado modelo de threading em um aplicativo. O aplicativo de nível superior deve indicar o modelo de threading. Dessa maneira, o aplicativo deverá ser livre para escolher a distribuição de eventos em seus threads, tornando o encapsulamento das listas de espera de eventos ainda mais importante.

Código simples, menos bugs

Resumindo, a associação de IDs não efêmeras a cada evento transmitido para as funções WaitForMultipleObjects do Windows pode levar à simplificação do código e reduzir a probabilidade de bugs, uma vez que remove a sobrecarga colocada sobre o aplicativo para converter índices de eventos sem sentido em identificadores ou ponteiros de objetos úteis. A classe DynWaitList encapsula com eficiência esse processo de associação em um wrapper ao redor dessas funções Wait da API do Windows. Todas as operações envolvidas são de ordem 1, exceto a construção e a remoção de identificadores, que são de ordem N. Uma melhor otimização pode ser obtida classificando-se a matriz, o que adicionaria uma pequena penalidade à adição do identificador e tornaria a remoção do identificador muito mais rápida.

Alex Gimenez trabalha atualmente como desenvolvedor próximo a Seattle, criando software de áudio/vídeo/sistemas de rede em tempo real com a equipe responsável pelo desenvolvimento do Microsoft Lync. Sua experiência anterior se estende por duas décadas lidando com software de ponto de vendas, incorporados, de drivers de dispositivos e de telecomunicações. Em seu tempo livre, ele pratica ciclismo, toca bateria e estuda japonês. Entre em contato com ele pelo email alexgim@microsoft.com.

Agradecemos aos seguintes especialistas técnicos pela revisão deste artigo: Bart Holmberg, Warren Lam e Jiannan Zheng