Este artigo foi traduzido por máquina.

Windows com C++

Timers e E/S do pool de threads

Kenny Kerr

Kenny Kerr
No presente, minha parcela final sobre o pool de threads do Windows 7, eu estou indo para cobrir os dois restantes geradora de retorno de chamada objetos fornecidos pela API. Há ainda mais eu poderia escrever sobre o pool de threads, mas após cinco artigos que cobrem praticamente todos os seus recursos, você deve ser confortável usando a potência do seus aplicativos de forma eficaz e eficiente.

No meu agosto (msdn.microsoft.com/magazine/hh335066) e novembro (msdn.microsoft.com/magazine/hh547107) colunas, descrevi trabalhar e esperar objetos respectivamente. Um objeto de trabalho permite-lhe apresentar trabalhos, sob a forma de uma função, diretamente para o pool de threads para execução. A função irá executar o mais rapidamente possível. Um objeto de espera informa o pool de threads para esperar por um objeto de sincronização do kernel em seu nome e enfileirar uma função quando ele é sinalizado. Esta é uma alternativa dimensionável para primitivos de sincronização tradicional e uma alternativa eficaz para sondagem. No entanto, existem muitos casos onde temporizadores são necessários para executar algum código após um determinado intervalo, ou algum período regular. Isso pode ser devido à falta de apoio "push" em algum protocolo Web ou talvez porque você está implementando um protocolo de comunicações UDP-estilo e você precisa manipular retransmissões. Felizmente, o pool de segmentos API fornece um objeto timer para lidar com todos esses cenários de forma eficiente e agora familiar.

Objetos de timer

A função CreateThreadpoolTimer cria um objeto timer. Se a função for bem-sucedido, ele retorna um ponteiro opaco que representa o objeto timer. Se ele falhar, ele retorna um valor de ponteiro nulo e fornece mais informações por meio da função GetLastError. Dado um objeto timer, a função CloseThreadpoolTimer informa o pool de threads que o objeto pode ser liberado. Se você está seguindo ao longo da série, isto deve tudo parecer bastante familiar. Aqui está uma classe de características que pode ser usada com o modelo de classe de unique_handle acessível apresentei na minha coluna de julho de 2011 (msdn.microsoft.com/magazine/hh288076):

struct timer_traits
{
  static PTP_TIMER invalid() throw()
  {
    return nullptr;
  }
  static void close(PTP_TIMER value) throw()
  {
    CloseThreadpoolTimer(value);
  }
};
typedef unique_handle<PTP_TIMER, timer_traits> timer;

Eu agora pode usar o typedef e criar um objeto timer da seguinte maneira:

void * context = ...
timer t(CreateThreadpoolTimer(its_time, context, nullptr));
check_bool(t);

Como de costume, o parâmetro final, opcionalmente, aceita um ponteiro para um ambiente que você possa associar o objeto timer com um ambiente, como descrevi na minha coluna de setembro de 2011 (msdn.microsoft.com/­revista/hh416747). O primeiro parâmetro é a função de retorno de chamada que será enfileirada para o pool de segmentos cada vez que o timer expire. O retorno de chamada do timer é declarado da seguinte maneira:

void CALLBACK its_time(PTP_CALLBACK_INSTANCE, void * context, PTP_TIMER);

Para controlar quando e com que freqüência o timer expirar, você usa a função SetThreadpoolTimer. Naturalmente, o primeiro parâmetro fornece o objeto timer, mas o segundo parâmetro indica o tempo devido a que o temporizador deve expirar. Ele usa uma estrutura FILETIME para descrever o tempo absoluto ou relativo. Se você não estiver certo de como isso funciona, encorajo-vos a ler a coluna do mês passado, onde descrevi a semântica em torno a estrutura FILETIME em detalhe. Aqui está um exemplo simples onde eu definir o timer para expirar em cinco segundos:

union FILETIME64
{
  INT64 quad;
  FILETIME ft;
};
FILETIME relative_time(DWORD milliseconds)
{
  FILETIME64 ft = { -static_cast<INT64>(milliseconds) * 10000 };
  return ft.ft;
}
auto due_time = relative_time(5 * 1000);
SetThreadpoolTimer(t.get(), &due_time, 0, 0);

Novamente, se você está inseguro sobre como funciona a função relative_time, por favor leia minha coluna de novembro de 2011. Neste exemplo, o timer expira depois de cinco segundos, altura em que o pool de threads irá enfileirar uma instância da função de retorno de chamada its_time. A menos que sejam tomadas medidas, não mais retornos de chamada serão enfileirados.

Você também pode usar SetThreadpoolTimer para criar um timer periódico que irá enfileirar um retorno de chamada em alguns intervalos regulares. Aqui está um exemplo:

auto due_time = relative_time(5 * 1000);
SetThreadpoolTimer(t.get(), &due_time, 500, 0);

Neste exemplo, retorno de chamada do timer é primeiro enfileirado depois de cinco segundos e, em seguida, cada meio segundo depois disso até que o objeto timer é redefinir ou fechado. Ao contrário o devido tempo, o período é simplesmente especificado em milissegundos. Tenha em mente que um timer periódico irá enfileirar um retorno de chamada depois de ultrapassado o período determinado, independentemente de quanto tempo leva o retorno de chamada para executar. Isso significa que é possível para Múltiplo retornos de chamada executar simultaneamente, ou sobreposição, se o intervalo é pequeno o suficiente ou os retornos de chamada demoram um tempo suficiente para executar.

Se você precisar garantir retornos de chamada não se sobreponham, e a hora de início precisas para cada período não é que importante, então uma abordagem diferente para a criação de um timer periódico pode ser apropriada. Em vez de especificar um período na chamada para SetThreadpoolTimer, simplesmente redefina o temporizador no retorno de chamada. Desta forma, você pode garantir que os retornos de chamada nunca serão sobrepostas. Se nada mais, isso simplifica a depuração. Imagine percorrendo um retorno de chamada do timer no depurador apenas para descobrir que o pool de segmentos já tem enfileirado algumas instâncias mais enquanto estavam analisando seu código (ou recarregar seu café). Com esta abordagem, que nunca vai acontecer. Aqui é o que parece:

void CALLBACK its_time(PTP_CALLBACK_INSTANCE, void *, PTP_TIMER timer)
{
  // Your code goes here
  auto due_time = relative_time(500);
  SetThreadpoolTimer(timer, &due_time, 0, 0);
}
auto due_time = relative_time(5 * 1000);
SetThreadpoolTimer(t.get(), &due_time, 0, 0);

Como você pode ver, o vencimento inicial é cinco segundos e, em seguida, redefinir o devido tempo para 500 ms no final de retorno de chamada. Eu tomei proveito do fato de que a assinatura de retorno de chamada fornece um ponteiro para o objeto timer originário, fazendo o trabalho de redefinir o timer muito simples. Você também pode querer usar RAII para garantir que a chamada para SetThreadpoolTimer confiável é chamada antes do retorno de chamada retorna.

Você pode chamar SetThreadpoolTimer com um valor de ponteiro nulo para o devido tempo parar qualquer encerramentos do timer futuras que podem resultar em mais retornos de chamada. Você também precisará chamar WaitForThreadpool­TimerCallbacks para evitar quaisquer condições de corrida. Naturalmente, objetos de timer funcionam igualmente bem com grupos de limpeza, conforme descrito na minha coluna de outubro de 2011.

Parâmetro final do SetThreadpoolTimer pode ser um pouco confuso porque a documentação refere-se a um "período de janela", bem como um atraso. O que é que tudo sobre? Esta é realmente uma característica que afeta a eficiência energética e ajuda a reduzir o poder global consumo. Baseia-se em uma técnica chamada temporizador coalescentes. Obviamente, a melhor solução é evitar temporizadores completamente e usar eventos em vez disso. Isso permite que os processadores do sistema maior quantidade de tempo ocioso, assim, incentivando-os a inserir seus Estados ociosos de baixo consumo de energia, tanto quanto possíveis. Ainda, se temporizadores são necessárias, temporizador coalescentes pode reduzir o geral consumo de energia, reduzindo o número de interrupções de timer que são necessários. Temporizador coalescentes baseia-se na idéia de um "atraso tolerável" para os encerramentos do timer. Dado algum atraso máximo admissível, o kernel do Windows pode ajustar o tempo de expiração real para coincidir com qualquer temporizadores existentes. Uma boa regra é definir o atraso a um décimo do período de uso. Por exemplo, se o timer deve expirar em 10 segundos, use um atraso de um segundo, dependendo o que é apropriado para seu aplicativo. Quanto maior for o atraso, mais a oportunidade que o kernel tem para otimizar seu timer interrompe. Por outro lado, nada menos do que 50 ms não será de grande utilidade porque ele começa a sobrepor-se no intervalo de relógio do kernel padrão.

Objetos de conclusão e/S

Agora é hora de me apresentar a jóia do thread pool API: objeto de conclusão a entrada/saída (e/S), ou simplesmente o objeto de e/S. Quando eu introduzido pela primeira vez o API do pool de threads, mencionei que o pool de segmentos é construído sobre a API de porta de conclusão de I/O. Tradicionalmente, a mais escalável e/S de execução no Windows era possível apenas usando a API de porta de conclusão de I/O. Eu tenho escrito sobre essa API no passado. Embora não é particularmente difícil de usar, nem sempre foi fácil de integrar com um aplicativo outros threading precisa. Graças à API do pool segmento, no entanto, você tem o melhor dos dois mundos com uma única API para trabalho, sincronização, temporizadores e agora i/O, também. Outro benefício é que executar sobreposto conclusão de I/O com o pool de segmentos é realmente mais intuitivo do que usando a API de porta de conclusão de I/O, especialmente quando se trata de lidar com vários identificadores de arquivo e vários sobreposto operações simultaneamente.

Como você deve ter adivinhado, a função CreateThreadpoolIo cria um objeto de e/S e a função CloseThreadpoolIo informa o pool de threads que o objeto pode ser liberado. Aqui é uma classe de características para o modelo de classe unique_handle:

struct io_traits
{
  static PTP_IO invalid() throw()
  {
    return nullptr;
  }
  static void close(PTP_IO value) throw()
  {
    CloseThreadpoolIo(value);
  }
};
typedef unique_handle<PTP_IO, io_traits> io;

A função CreateThreadpoolIo aceita um identificador de arquivo, implicando que um objeto de e/S é capaz de controlar a e/S para um único objeto. Naturalmente, que objeto precisa ter suporte a e/S sobrepostos, mas isso inclui tipos de recursos populares tais como ficheiros de sistema de arquivos, pipes nomeados, soquetes e assim por diante. Permitam-me que demonstrar com um exemplo simples de espera para receber um pacote UDP usando um soquete. Para gerenciar o soquete, usarei unique_handle com a classe de características seguintes:

struct socket_traits
{
  static SOCKET invalid() throw()
  {
    return INVALID_SOCKET;
  }
  static void close(SOCKET value) throw()
  {
    closesocket(value);
  }
};
typedef unique_handle<SOCKET, socket_traits> socket;

Ao contrário das classes de traços que mostrei, até agora, neste caso a função inválida não retorna um valor de ponteiro nulo. Isso ocorre porque a função WSASocket, como a função CreateFile, usa um valor incomum para indicar um identificador inválido. Dada esta classe de características e typedef, eu posso criar um socket e objeto e/S muito simplesmente:

socket s(WSASocket( ...
, WSA_FLAG_OVERLAPPED));
check_bool(s);
void * context = ...
io i(CreateThreadpoolIo(reinterpret_cast<HANDLE>(s.get()), io_completion, context, nullptr));
check_bool(i);

A função de retorno de chamada que sinaliza a conclusão de qualquer operação de e/S é declarada da seguinte maneira:

void CALLBACK io_completion(PTP_CALLBACK_INSTANCE, void * context, void * overlapped,
  ULONG result, ULONG_PTR bytes_copied, PTP_IO)

Os parâmetros exclusivos para esse retorno de chamada devem estar familiarizados se você usou a e/S sobreposto antes. Porque sobreposto e/S é por natureza assíncrona e permite a sobreposição de operações e/S — daí o nome sobreposto I / O — é preciso haver uma maneira de identificar a operação de e/S particular que foi concluída. Este é o propósito do parâmetro sobreposto. Este parâmetro fornece um ponteiro para a estrutura OVERLAPPED ou WSAOVERLAPPED que foi especificado quando uma determinada operação de e/S foi iniciada pela primeira vez. A abordagem tradicional de embalagem uma estrutura OVERLAPPED para uma estrutura maior para pendurar mais dados fora deste parâmetro pode ainda ser usada. O parâmetro sobreposto fornece uma maneira para identificar a operação de e/S particular que foi concluída, enquanto o parâmetro de contexto — como de costume — fornece um contexto para o ponto de extremidade I/O, independentemente de qualquer operação específica. Tendo em conta estes dois parâmetros, você deve ter nenhuma dificuldade em coordenar o fluxo de dados através de seu aplicativo. O parâmetro de resultado informa se a operação sobreposta sucedeu com a habitual ERROR_SUCCESS ou zero, indicando o sucesso. Finalmente, o parâmetro bytes_copied, obviamente, diz-lhe quantos bytes foram realmente lido ou gravados. Um erro comum é supor que o número de bytes solicitados na verdade foi copiado. Não cometer esse erro: ele é a razão para a existência do parâmetro.

A única parte do suporte de i/O do pool de threads que é um pouco complicado é a manipulação da solicitação de i/O próprio. Ele cuida para codificar isso corretamente. Antes de chamar uma função para iniciar alguma operação de e/S assíncrona, tais como ReadFile ou WSARecvFrom, você deve chamar a função StartThreadpoolIo para informar o pool de threads que uma operação de e/S está prestes a começar. O truque é que, se a operação de e/S acontece para concluir de forma síncrona, em seguida, você deve notificar o pool de segmentos de isso chamando a função CancelThreadpoolIo. Tenha em mente que conclusão I/O não equivale necessariamente a conclusão com êxito. Uma operação de e/S pode ter sucesso ou falhar tanto de forma síncrona ou assíncrona. De qualquer forma, se a operação de e/S não notificará a porta de conclusão da sua conclusão, você precisará informar o pool de segmentos. Aqui é que isso pode parecer no contexto de receber um pacote UDP:

StartThreadpoolIo(i.get());
auto result = WSARecvFrom(s.get(), ...
if (!result)
{
  result = WSA_IO_PENDING;
}
else
{
  result = WSAGetLastError();
}
if (WSA_IO_PENDING != result)
{
  CancelThreadpoolIo(i.get());
}

Como você pode ver, eu começar o processo chamando StartThreadpoolIo para dizer o pool de threads que uma operação de e/S está prestes a começar. Eu, em seguida, chamar WSARecvFrom para que as coisas vão. Interpretar o resultado é a parte mais importante. A função WSARecvFrom retorna zero se a operação foi concluída com êxito, mas a porta de conclusão será ainda notificado, assim que eu mudar o resultado de WSA_IO_PENDING. Qualquer outro resultado de WSARecvFrom indica falha, com a exceção, é claro, de WSA_IO_PENDING, que significa simplesmente que a operação foi iniciada com êxito, mas ele será completado posteriormente. Agora, basta chamar CancelThreadpoolIo se o resultado não está pendente para manter o pool de segmentos até a velocidade. Os pontos de extremidade de I/O diferentes podem fornecer diferentes semânticas. Por exemplo, / S de arquivo pode ser configurado para evitar notificando a porta de conclusão após a conclusão síncrona. Você precisará chamar CancelThreadpoolIo conforme apropriado.

Como os outros geradores de retorno de chamada objetos no pool de segmentos API, enquanto se aguarda a retornos de chamada para e/S objetos podem ser cancelados usando a função WaitForThreadpoolIoCallbacks. Basta ter em mente que isso irá cancelar qualquer pendentes retornos de chamada, mas não cancelar qualquer pendente operações de e/S-se. Você ainda precisará usar a função apropriada para cancelar a operação para evitar quaisquer condições de corrida. Isso permite que você livre qualquer estruturas OVERLAPPED com segurança e assim por diante.

E isso é tudo para o pool de segmentos API. Como eu disse, há mais eu poderia escrever sobre essa poderosa API, mas dado a passo a passo detalhada que forneci, até agora, eu tenho certeza que você está bem no seu caminho para usá-lo para seu próximo aplicativo de energia. Juntar-me próximo mês como eu continuar a explorar o Windows com C++.

Kenny Kerr é um artesão de software com uma paixão para desenvolvimento Windows nativo. Contatá-lo em kennykerr.ca.