Este artigo foi traduzido por máquina.

Windows com C++

Sincronização do pool de threads

Kenny Kerr

 

Kenny KerrEu já disse isso antes: operações de bloqueio são más notícias para simultaneidade. Em geral, no entanto, você precisa aguardar algum recurso se torne disponível ou talvez você esteja implementando um protocolo que estipula que precisa de algum tempo decorrido antes de reenviar um pacote de rede. O que fazer então? Você poderia usar uma seção crítica, chamar funções, como suspensão e WaitForSingleObject e assim por diante. Obviamente, isso significa que você terá de threads em bloqueio novamente. Você precisa é uma maneira para o pool de segmentos de espera em seu nome sem afetar seus limites de simultaneidade, que abordei na minha coluna de setembro de 2011 (msdn.microsoft.com/magazine/hh394144). O pool de segmentos pode, em seguida, na fila um retorno de chamada depois que o recurso está disponível ou o tempo decorrido.

Na coluna deste mês, vou mostrar como você pode fazer exatamente isso. Junto com objetos de trabalho, que apresentei na minha coluna de agosto de 2011 (msdn.microsoft.com/magazine/hh335066), o pool de segmentos API fornece uma série de outros objetos da geração de retorno de chamada. Este mês, vou mostrar como usar objetos de espera.

Objetos de Espera

Objeto de espera do pool de segmentos é usado para sincronização. Em vez de bloquear uma seção crítica — ou slim bloqueio de leitor/gravador — você pode esperar para um objeto de sincronização do kernel, geralmente um evento ou um semáforo, fique sinalizado.

Embora você possa usar WaitForSingleObject e amigos, um objeto de espera se integra perfeitamente com o restante da API do pool de segmentos. Isso é bastante eficiente pelo agrupamento de quaisquer objetos de espera que você envia, reduzindo o número de segmentos necessários e a quantidade de código que você precisa escrever e depurar. Isso permite que você use um ambiente de pool de segmentos e grupos de limpeza e também elimina a necessidade que dedicar um ou mais threads para esperar objetos ficar sinalizado. Devido a melhorias na parte do kernel do pool de threads, ele pode em alguns casos até mesmo obter isso de maneira threadless.

A função CreateThreadpoolWait cria um objeto de espera. Se a função tiver êxito, ele retorna um ponteiro opaco, que representa o objeto de espera. Caso contrário, ela retorna um ponteiro nulo e fornece mais informações através da função GetLastError. A função de CloseThreadpoolWait devido a um objeto de trabalho, informa o pool de segmentos a que o objeto pode ser liberado. Esta função não retorna um valor e de eficiência pressupõe que o objeto de espera é válido.

O modelo de classe unique_handle que apresentei na minha coluna de julho de 2011 (msdn.microsoft.com/magazine/hh288076) se encarrega desses detalhes.

Aqui está uma classe de características que pode ser usada com unique_handle, e uma definição de tipo por questão de praticidade:

struct wait_traits
{
  static PTP_WAIT invalid() throw()
  {
    return nullptr;
  }
 
  static void close(PTP_WAIT value) throw()
  {
    CloseThreadpoolWait(value);
  }
};
 
typedef unique_handle<PTP_WAIT, wait_traits> wait;

Agora posso usar o typedef e crie um objeto de espera da seguinte maneira:

void * context = ...
wait w(CreateThreadpoolWait(callback, context, nullptr));
check_bool(w);

Como sempre, o parâmetro final, opcionalmente, aceita um ponteiro para um ambiente para que você pode associar o objeto de espera um ambiente, como descrevi na minha coluna de setembro. O primeiro parâmetro é a função de retorno de chamada que será enfileirada para o pool de threads quando a espera for concluída. O retorno de chamada de espera é declarado da seguinte maneira:

void CALLBACK callback(PTP_CALLBACK_INSTANCE, void * context, PTP_WAIT, TP_WAIT_RESULT);

O argumento TP_WAIT_RESULT de retorno de chamada é apenas um inteiro sem sinal fornecendo o motivo por que a espera foi concluída. Um valor WAIT_OBJECT_0 indica que a espera foi atendida como o objeto de sincronização ficou sinalizado. Como alternativa, um valor WAIT_TIMEOUT indica que o intervalo de tempo limite decorrido antes que o objeto de sincronização foi sinalizado. Como você indicaria o objeto de sincronização e o tempo limite para aguardar? Esse é o trabalho da função SetThreadpoolWait surpreendentemente complexa. Essa função é bastante simple até que você tente especificar um tempo limite. Considere este exemplo:

handle e(CreateEvent( ...
));
check_bool(e);
 
SetThreadpoolWait(w.get(), e.get(), nullptr);

Primeiro, crio um objeto de evento, usando o typedef unique_handle da minha coluna de julho. Não é surpresa que a função SetThreadpoolWait define o objeto de sincronização que o objeto de espera deve aguardar. O último parâmetro indica um tempo limite opcional, mas neste exemplo, posso fornecer um valor de ponteiro nulo, indicando que o pool de segmentos deve esperar indefinidamente.

A estrutura FILETIME

Mas e quanto tempo um limite específico? É onde fica complicada. Funções como WaitForSingleObject permitem que você definir um valor de tempo limite em milissegundos, como um inteiro não assinado. A função SetThreadpoolWait, no entanto, espera um ponteiro para uma estrutura FILETIME, que apresenta alguns desafios para o desenvolvedor. A estrutura FILETIME é um valor de 64 bits que representa uma data absoluta e o tempo decorrido desde o início do ano 1601 em intervalos de 100 nanossegundos (com base no tempo Universal Coordenado).

Para acomodar os intervalos de tempo relativo, SetThreadpoolWait trata a estrutura FILETIME como um valor de 64 bits assinado. Se for fornecido um valor negativo, ele obtém o valor não assinado como um intervalo de tempo em relação à hora atual, novamente em intervalos de 100 nanossegundos. Vale a pena mencionar que o timer relativo pára de contagem quando o computador está em suspensão ou hibernação. Obviamente, os valores de tempo limite absoluto não são afetados por isso. Mesmo assim, este uso FILETIME não é conveniente para qualquer um dos valores de tempo limite de absoluto ou relativo.

Provavelmente, a abordagem mais simples para os tempos limite absoluto é preencher a estrutura SYSTEMTIME e, em seguida, use a função de SystemTimeToFileTime para preparar uma estrutura FILETIME para você:

SYSTEMTIME st = {};
st.wYear = ...
st.wMonth = ...
st.wDay = ...
st.wHour = ...
// etc.
FILETIME ft;
check_bool(SystemTimeToFileTime(&st, &ft));
 
SetThreadpoolWait(w.get(), e.get(), &ft);

Para valores de tempo limite relativo, pensar mais um pouco está envolvido. Primeiro, você precisará converter algum tempo relativo em intervalos de 100 nanossegundos e, em seguida, convertê-lo para um valor negativo de 64 bits. Este último é mais complicado do que parece. Lembre-se de que os computadores representam inteiros assinados usando das duas complemento sistema, com o efeito que um valor negativo deve ter o bit mais significativo definido como alto. Adicionado a este é o fato de que FILETIME, na verdade, consiste em dois valores de 32 bits. Isso significa que você também precisa lidar com o alinhamento de máquina corretamente quando tratá-la como um valor de 64 bits, caso contrário, uma falha de alinhamento pode ocorrer. Além disso, você não pode simplesmente usar os valores mais baixos de 32 bits para armazenar o valor, como o bit mais significativo é os valores mais altos de 32 bits.

Conversão do valor de tempo limite relativo

É comum para express relativo tempos limite em milissegundos, portanto, deixe-me demonstram essa conversão aqui. Lembre-se de que é de um milissegundo de milésimos de segundo e um nanossegundos é uma bilionésima parte de um segundo. Outra maneira de vê-la é que um milissegundo é 1.000 microssegundos e um microssegundos é 1.000 nanossegundos. Um milissegundo é, então, 10.000 unidades de 100 nanossegundos, a unidade de medida esperada pelo SetThreadpoolWait. Existem muitas maneiras de expressar isso, mas aqui está uma abordagem que funciona:

DWORD milliseconds = ...
auto ft64 = -static_cast<INT64>(milliseconds) * 10000;
 
FILETIME ft;
memcpy(&ft, &ft64, sizeof(INT64));
 
SetThreadpoolWait(w.get(), e.get(), &ft);

Observe que estou cuidado para converter o valor de DWORD antes da multiplicação para evitar o estouro de inteiros. Eu também uso memcpy, pois um reinterpret_cast exigiria o FILETIME seja alinhado em um limite de 8 bytes. Você poderia, claro, fazer isto em vez disso, mas isso é um pouco mais limpo. Uma abordagem ainda mais simples se beneficia do fato de que o compilador Visual C++ alinha uma união com a maior necessidade de alinhamento de qualquer um dos seus membros. Na verdade, se você ordenar os membros da união corretamente, você pode fazer isso em apenas uma linha, da seguinte maneira:

union FILETIME64
{
  INT64 quad;
  FILETIME ft;
};
 
FILETIME64 ft = { -static_cast<INT64>(milliseconds) * 10000 };
 
SetThreadpoolWait(w.get(), e.get(), &ft.ft);

Suficiente truques do compilador. Voltemos ao pool de segmentos. Outra coisa, que você poderá ficar tentado a experimentar é o tempo limite zero. Isso geralmente é feito usando WaitForSingleObject como uma maneira de determinar se um objeto de sincronização é sinalizado sem realmente o bloqueio e aguardando. No entanto, esse procedimento não é suportado pelo pool de segmentos, ficando assim melhor continuar com WaitForSingleObject.

Se desejar que um objeto de trabalho específico para cessar o seu objeto de sincronização de aguardar, em seguida, basta chame SetThreadpoolWait com um valor de ponteiro nulo como seu segundo parâmetro. Cuidado apenas com a condição de corrida óbvia.

A função final relacionada a objetos de espera é WaitForThreadpoolWaitCallbacks. A princípio, pode parecer semelhante à função WaitForThreadpoolWorkCallbacks usada com objetos de trabalho, que apresentei na minha coluna de agosto. Não deixe que ele enganá-lo. A função WaitForThreadpoolWaitCallbacks literalmente faz o nome sugere. Ele espera para qualquer retornos de chamada a partir do objeto de espera específico.

O problema é que o objeto de espera apenas enfileirar um retorno de chamada quando o objeto de sincronização associado é sinalizado ou quando o tempo limite expirar. Até que um desses eventos ocorre, nenhum retorno de chamada é enfileirados e não há nada para a função de espera aguardar. A solução é primeiro chamar SetThreadpoolWait com valores de ponteiro nulo, informando o objeto de espera para cessar o espera e depois chamar o WaitForThreadpoolWaitCallbacks para evitar quaisquer condições de corrida:

SetThreadpoolWait(w.get(), nullptr, nullptr);
WaitForThreadpoolWaitCallbacks(w.get(), TRUE);

Como você poderia esperar, o segundo parâmetro determina se qualquer chamada de retorno que pode ter falhou, mas ainda não foram iniciadas executar será cancelada ou não. Naturalmente, espere o trabalho de objetos bem com grupos de limpeza. Você pode ler meu de 2011 de outubro (msdn.microsoft.com/magazine/hh456398) a coluna para descobrir como usar os grupos de limpeza. Em projetos maiores, eles realmente ajudar a simplificar muito o cancelamento e a limpeza que precisa ser feito mais complicado.

 

Kenny Kerr* é um profissional de fabricação de software apaixonado pelo desenvolvimento nativo para Windows. Entre em contato com Kenny em kennykerr.ca.*

Graças ao especialista técnico seguir pela revisão deste artigo: Pedro Teixeira