Este artigo foi traduzido por máquina.

Windows com C++

Cancelamento e limpeza do pool de threads

Kenny Kerr

 

Kenny KerrCancelamento e limpeza são notoriamente difíceis de problemas para resolver quando se trata de aplicativos multithread. Quando é seguro fechar uma alça? É importante segmento que cancela uma operação? Para piorar as coisas, algumas APIs multithread não são reentrantes, potencialmente aumentando o desempenho, mas também acrescentar complexidade para o desenvolvedor.

Apresentei o ambiente do pool de threads na coluna do mês passado (msdn.microsoft.com/magazine/hh394144). Um recurso crítico, permite que esse ambiente é grupos de limpeza e que é o que eu me concentrarei em aqui. Grupos de limpeza não tentam resolver todos os cancelamento e problemas de limpeza do mundo. O que fazer é tornar os objetos e retornos de chamada do pool de segmentos mais gerenciáveis e indiretamente isso pode ajudar a simplificar o cancelamento e a limpeza de outras APIs e os recursos conforme necessário.

Até agora, apenas mostrei como usar o modelo de classe unique_handle para fechar automaticamente os objetos de trabalho por meio da função CloseThreadpoolWork. (Consulte a coluna de agosto de 2011 em msdn.microsoft.com/magazine/hh335066 para obter detalhes.) Existem algumas limitações com essa abordagem, entretanto. Se você quiser um digamos em se pendentes retornos de chamada são canceladas ou não, você precisará chamar depois WaitForThreadpoolWorkCallbacks primeiro. Que faz chamadas de função de dois objetos de multipliednumber de geração de retorno de chamada em uso por seu aplicativo. Se você optar por usar TrySubmitThreadpoolCallback, você ainda não ter a oportunidade de fazê-lo e é deixados se perguntando como cancelar ou aguarde o retorno de chamada resultante. Obviamente, um aplicativo real provavelmente terá muito mais do que apenas os objetos de trabalho. Na coluna do próximo mês, irei começar apresentando os outros objetos de pool de segmentos que produzem retornos de chamada, de cronômetros para i/O para objetos waitable. Coordenar o cancelamento e a limpeza de todas essas pode rapidamente se tornar um pesadelo. Felizmente, os grupos de limpeza resolvem esses problemas e muito mais.

A função CreateThreadpoolCleanupGroup cria um objeto de grupo de limpeza. Se a função obtiver êxito, ele retorna um ponteiro opaco, que representa o objeto de grupo de limpeza. Caso contrário, ela retorna um ponteiro nulo e fornece mais informações através da função GetLastError. A função CloseThreadpoolCleanupGroup devido a um objeto de grupo de limpeza, instrui o pool de segmentos a que o objeto pode ser liberado. Já mencionei isso antes de passagem, mas é importante lembrar — a API do pool de thread não tolerar argumentos inválidos. Chamar CloseThreadpoolCleanupGroup ou qualquer outra API funciona com uma inválido, fechada anteriormente ou valor de ponteiro nulo fará com que seu aplicativo falhar. Estes são introduzidos pelo programador de defeitos e não devem exigir verificações adicionais em tempo de execução. O modelo de classe unique_handle que apresentei na minha coluna de julho de 2011 (msdn.microsoft.com/magazine/hh288076) cuida desses detalhes com a Ajuda de uma classe específica de limpeza-grupo características:

struct cleanup_group_traits
{
  static PTP_CLEANUP_GROUP invalid() throw()
  {
    return nullptr;
  }
 
  static void close(PTP_CLEANUP_GROUP value) throw()
  {
    CloseThreadpoolCleanupGroup(value);
  }
};
typedef unique_handle<PTP_CLEANUP_GROUP, cleanup_group_traits> cleanup_group;

Agora posso usar o typedef conveniente e criar um objeto de grupo de limpeza da seguinte maneira:

cleanup_group cg(CreateThreadpoolCleanupGroup());
check_bool(cg);

Como em particulares pools e prioridades de retorno de chamada, um grupo de limpeza está associado a vários objetos de geração de retorno de chamada por meio de um objeto de ambiente. Primeiro, atualize o ambiente para indicar o grupo de limpeza que gerenciará o tempo de vida dos objetos e seus retornos de chamada, como este:

environment e;
SetThreadpoolCallbackCleanupGroup(e.get(), cg.get(), nullptr);

Neste ponto, você pode adicionar objetos ao grupo de limpeza que são referenciadas como membros do grupo de limpeza. Esses objetos também podem ser removidos individualmente do grupo de limpeza, mas é mais comum para fechar todos os membros em uma única operação.

Um objeto de trabalho pode se tornar um membro de um grupo de limpeza no momento da criação simplesmente fornecendo o ambiente atualizado para a função CreateThreadpoolWork:

auto w = CreateThreadpoolWork(work_callback, nullptr, e.get());
check_bool(nullptr != w);

Observe que eu não uso um unique_handle neste momento. O objeto de trabalho recém-criado agora é um membro do grupo de limpeza do ambiente e sua vida útil não precisa ser controlada diretamente usando RAII.

É possível revogar a participação do objeto trabalho somente por fechá-lo, o que pode ser feito individualmente com a função CloseThreadpoolWork. O pool de segmentos sabe que o objeto de trabalho é um membro do grupo de limpeza e revoga sua participação antes de fechá-lo. Isso garante que o aplicativo não apresentar falha quando o grupo de limpeza tenta fechar todos os seus membros mais tarde. O inverso não é verdade: se você primeiro instruir o grupo de limpeza para fechar todos os seus membros e, em seguida, chama CloseThreadpoolWork no objeto de trabalho agora inválido, ocorrerá falha em seu aplicativo.

É claro que, toda a idéia de um grupo de limpeza é liberar o aplicativo precisar fechar individualmente todos os objetos diversas geradores de retorno de chamada, que ele acaso está usando. Mais importante, ele permite que o aplicativo para aguardar e opcionalmente Cancelar quaisquer chamadas de retorno pendentes em uma operação de espera único em vez de precisar ter um segmento do aplicativo de espera e retomar repetidamente. A função CloseThreadpoolCleanupGroupMembers fornece todos esses serviços e muito mais:

bool cancel = ...
CloseThreadpoolCleanupGroupMembers(cg.get(), cancel, nullptr);

Esta função pode parecer simple, mas na realidade, ele executa várias tarefas importantes como um agregado em todos os seus membros. Primeiro, dependendo do valor do segundo parâmetro, ele cancela qualquer retornos de chamada pendentes que ainda não esteja em execução. Em seguida, ele aguarda os retornos de chamada que já começaram a executar e, opcionalmente, quaisquer chamadas de retorno pendentes se você optou por não cancelá-las. Finalmente, ele fecha todos os seus objetos de membro.

Alguns têm comparado a grupos de limpeza a coleta de lixo, mas acho que essa é uma metáfora enganosa. Se qualquer coisa, um grupo de limpeza é mais semelhante a um recipiente de biblioteca STL (Standard Template) de geração de retorno de chamada objetos. Os objetos adicionados a um grupo não serão fechados automaticamente por qualquer motivo. Se você não conseguir chamar CloseThreadpoolCleanupGroupMembers, seu aplicativo causarem perda de memória. Chamar CloseThreadpoolCleanupGroup para fechar o grupo em si não ajude. Em vez disso, basta Pense um grupo de limpeza como uma maneira de gerenciar o tempo de vida e a simultaneidade de um grupo de objetos. Obviamente, você pode criar vários grupos de limpeza em seu aplicativo para gerenciar grupos diferentes de objetos individualmente. É uma abstração incrivelmente útil — mas não é mágica, e deve ter cuidado para usá-lo corretamente. Considere o seguinte pseudocódigo com defeito:

environment e;
SetThreadpoolCallbackCleanupGroup(e.get(), cg.get(), nullptr);
 
while (app is running)
{
  SubmitThreadpoolWork(CreateThreadpoolWork(work_callback, nullptr, e.get()));
 
  // Rest of application.
}
 
CloseThreadpoolCleanupGroupMembers(cg.get(), true, nullptr);

Com previsibilidade, esse código irá usar uma quantidade não vinculada de memória e obterá mais lento e mais lentamente, como recursos do sistema estão esgotados.

No meu coluna de agosto 2011, eu demonstrei que a função TrySubmitThreadpoolCallback aparentemente simple é bastante problemática, pois há nenhuma maneira simple para aguardar o seu retorno de chamada concluir. Isso ocorre porque o objeto de trabalho não é realmente exposto pela API. O pool de segmentos propriamente dito, no entanto, sofre há essa restrição. Porque TrySubmitThreadpoolCallback aceita um ponteiro para um ambiente, você pode indiretamente tornar o trabalho resultante do objeto membro de um grupo de limpeza. Dessa forma, você pode usar o CloseThreadpoolCleanupGroupMembers aguardar ou cancelar o retorno de chamada resultante. Considere o seguinte pseudocódigo aprimorado:

environment e;
SetThreadpoolCallbackCleanupGroup(e.get(), cg.get(), nullptr);
 
while (app is running)
{
  TrySubmitThreadpoolCallback(simple_callback, nullptr, e.get());
 
  // Rest of application.
}
 
CloseThreadpoolCleanupGroupMembers(cg.get(), true, nullptr);

Quase que eu poderia Perdoe um desenvolvedor para pensar que isso é semelhante a coleta de lixo, porque o pool de segmentos fecha automaticamente o objeto de trabalho criado por TrySubmitThreadpoolCallback. Obviamente, isso tem nada a ver com grupos de limpeza. Descrevi esse comportamento na minha coluna de julho de 2011. Nesse caso a função CloseThreadpoolCleanupGroupMembers não é inicialmente responsável por fechar o objeto de trabalho, mas apenas para aguardar e possivelmente cancelar retornos de chamada. Diferentemente do exemplo anterior, este será executado indefinidamente sem usar recursos indevidos e ainda fornecer cancelamento de 
predictable e limpeza. Com a Ajuda dos grupos de retorno de chamada, TrySubmitThreadpoolCallback redeems propriamente dito, oferecendo uma alternativa segura e conveniente. Em um aplicativo altamente estruturado, onde a mesma chamada de retorno é enfileirada repetidamente, ainda seria mais eficiente para reutilizar um objeto de trabalho explícitas, mas a conveniência dessa função não pode ser descartada.

Os grupos de limpeza fornecem um recurso final para simplificar os requisitos de limpeza do seu aplicativo. Em geral, não é suficiente simplesmente aguardar chamadas de retorno pendentes concluir. Talvez você precise realizar algumas tarefas de limpeza para cada objeto geradores de retorno de chamada depois que tiver certeza de que nenhuma outra retornos de chamada serão executado. Ter um grupo de limpeza para gerenciar o tempo de vida desses objetos também significa que o pool de segmentos na melhor posição para saber quando as tarefas de limpeza, devem acontecer.

Quando você associar um grupo de limpeza com um ambiente por meio da função SetThreadpoolCallbackCleanupGroup, você também pode fornecer um retorno de chamada a ser executado para cada membro do grupo de limpeza como parte do processo da função CloseThreadpoolCleanupGroupMembers de fechar esses objetos. Porque este é um atributo do ambiente, você mesmo pode aplicar diferentes retornos de chamada para diferentes objetos que pertencem ao mesmo grupo de limpeza. No exemplo a seguir, eu crio um ambiente para o grupo de limpeza e o retorno de chamada de limpeza:

void CALLBACK cleanup_callback(void * context, void * cleanup)
{
  printf("cleanup_callback: context=%s cleanup=%s\n", context, cleanup);
}
 
environment e;
SetThreadpoolCallbackCleanupGroup(e.get(), cg.get(), cleanup_callback);

O parâmetro primeiro do retorno de chamada a limpeza é o valor de contexto para o objeto geradores de retorno de chamada. Esse é o valor de contexto especificado ao chamar as funções de CreateThreadpoolWork ou TrySubmitThreadpoolCallback, por exemplo, e é como você sabe qual objeto que o retorno de chamada de limpeza está sendo chamado para. Segundo parâmetro do retorno de limpeza é o valor fornecido como o último parâmetro ao chamar a função CloseThreadpoolCleanupGroupMembers.

Agora, considere os seguintes objetos de trabalho e os retornos de chamada:

void CALLBACK work_callback(PTP_CALLBACK_INSTANCE, void * context, PTP_WORK)
{
  printf("work_callback: context=%s\n", context);
}
 
void CALLBACK simple_callback(PTP_CALLBACK_INSTANCE, void * context)
{
  printf("simple_callback: context=%s\n", context);
}
 
SubmitThreadpoolWork(CreateThreadpoolWork(work_callback, "Cheetah", e.get()));
SubmitThreadpoolWork(CreateThreadpoolWork(work_callback, "Leopard", e.get()));
check_bool(TrySubmitThreadpoolCallback(simple_callback, "Meerkat", e.get()));

Qual destas opções não é como as outras? Tanto quanto o Meerkat pouco bonita quer ser exatamente como seus vizinhos gatos grandes na África do Sul, ele simplesmente nunca será uma delas. O que acontece quando os membros do grupo de limpeza são fechados como a seguir?

CloseThreadpoolCleanupGroupMembers(cg.get(), true, "Cleanup");

Muito pouco é determinado no código multithread. Se gerenciam os retornos de chamada executar antes que eles são cancelados e fechados, a seguir pode ser impresso:

work_callback: context=Cheetah
work_callback: context=Leopard
simple_callback: context=Meerkat
cleanup_callback: context=Cheetah cleanup=Cleanup
cleanup_callback: context=Leopard cleanup=Cleanup

Um erro comum é considerar que o retorno de chamada de limpeza é chamado somente para objetos cujos retornos de chamada não obteve uma oportunidade para executar. A API do Windows é um pouco enganador porque ele se refere às vezes, para o retorno de chamada de limpeza como um retorno de chamada ' Cancelar ', mas isso não for o caso. O retorno de chamada de limpeza é denominado simplesmente para todos os atuais membros do grupo de limpeza. Você poderia pensá-lo como um destruidor de membros do grupo de limpeza, mas, assim como acontece com a metáfora de coleta de lixo, isso não é sem risco. Esta metáfora acomoda até pretty bem até chegar à função TrySubmitThreadpoolCallback, que apresenta mais uma vez uma complicação. Lembre-se de que o pool de segmentos fecha automaticamente o objeto de trabalho subjacente que essa função cria assim que executa seu retorno de chamada. Isso significa que se deve ou não o retorno de chamada de limpeza é executada para este objeto de trabalho implícito depende ou não seu retorno de chamada já começou a ser executado no momento em que é chamada CloseThreadpoolCleanupGroupMembers. O retorno de chamada de limpeza só será executado para este objeto de trabalho implícito se seu retorno de chamada de trabalho é ainda pendente e você pergunte CloseThreadpoolCleanupGroupMembers cancelar qualquer chamada de retorno. Isso é tudo isso em vez disso, imprevisível e, portanto, não recomendo usar TrySubmitThreadpoolCallback com um retorno de chamada de limpeza.

Finalmente, vale a pena mencionar que embora blocos CloseThreadpoolCleanupGroupMembers, ele não perder qualquer tempo. Todos os objetos que estão prontos para limpeza terão seus retornos de chamada de limpeza executados no thread de chamada, enquanto aguarda outras chamadas de retorno pendentes concluir. Os recursos fornecidos por grupos de limpeza — e, em particular, a função CloseThreadpoolCleanupGroupMembers — são imprescindíveis para subdividir ou partes do seu aplicativo eficiente e corretamente.

Kenny Kerr é um profissional de fabricação de software apaixonado pelo desenvolvimento nativo para Windows. Contatá-lo em kennykerr.ca

Graças ao seguir técnico especialista para revisar este artigo: Hari Pulapaka