Windows com C++

A evolução de threads e E/S no Windows

Kenny Kerr

 

Kenny KerrAo começar um novo projeto, você se pergunta se seu programa será limitado pela computação ou pela E/S? Pois deveria. Descobri que na maioria das vezes, ou é por um, ou por outro. Você pode estar trabalhando em uma biblioteca analítica que lhe fornece uma pilha de dados e mantém um monte de processadores ocupados enquanto junta esses dados em um conjunto de agregações. Como alternativa, seu código pode passar a maior parte do tempo aguardando que aconteça alguma coisa, que os dados cheguem pela rede, que um usuário clique em algo ou execute algum gesto complexo com seis dedos. Nesse caso, os threads do seu programa não estão fazendo muita coisa. Obviamente, há casos em que os programas são pesados na E/S e na computação. O mecanismo de banco de dados do SQL Server vem à cabeça, mas ele é menos comum na programação de computadores dos dias atuais. Frequentemente, seu programa é encarregado de coordenar o trabalho de outros. Pode ser um servidor Web ou um cliente se comunicando com um banco de dados SQL, transmitindo parte da computação à GPU ou apresentando algum conteúdo para que o usuário interaja com ele. Dados todos esses diferentes cenários, como você decide quais recursos de threading seu programa exige e quais blocos de construção simultâneos são necessários ou úteis? De modo geral, essa é uma pergunta difícil de responder e algo que você precisará analisar à medida que um novo projeto se aproxima. No entanto, é útil entender a evolução do threading no Windows e no C++ para que você possa tomar uma decisão informada com base nas opções práticas que estão disponíveis.

Obviamente, os threads não fornecem nenhum valor direto ao usuário. Seu programa não será mais assombroso se você usar duas vezes mais threads quanto outro programa. É o que você faz com esses threads que realmente conta. Para ilustrar essas ideias e de que modo o threading evoluiu ao longo do tempo, vou mostrar o exemplo de leitura de alguns dados de um arquivo. Deixarei de focar as bibliotecas do C e do C++, pois o suporte delas para a E/S é, na maioria das vezes, direcionado para a E/S síncrona, ou de bloqueio, o que geralmente não nos interessa, a menos que você esteja criando um programa de console simples. É claro que não há nada de errado com isso. Alguns dos meus programas favoritos são os de console, que tem uma função e a executam muito bem. Mesmo assim, isso não é muito interessante, então seguirei adiante.

Um thread

Antes de mais nada, começarei com a API do Windows e a boa e velha (e convenientemente denominada) função ReadFile. Para que eu possa começar lendo o conteúdo de um arquivo, preciso de uma identificação para o arquivo, que é fornecida pela função imensamente potente CreateFile:

 

auto fn = L"C:\\data\\greeting.txt"; auto f = CreateFile(fn, GENERIC_READ, FILE_SHARE_READ, nullptr,   OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); ASSERT(f);

Para que os exemplos não se prolonguem muito, usarei apenas as macros ASSERT e VERIFY como espaços reservados para indicar onde você precisará adicionar alguma identificação de erro a fim de gerenciar as falhas informadas pelas várias funções da API. Nesse trecho de código, a função CreateFile é usada para abrir, e não para criar o arquivo. A mesma função é usada para ambas as operações. A especificação Create no nome é mais pelo fato de que é criado um objeto de arquivo do kernel, e não tanto pelo fato de um arquivo ser ou não criado no sistema de arquivos. Os parâmetros são bem autoexplicativos e não tão relevantes para esta discussão, com exceção do segundo ao último, que permitem especificar um conjunto de sinalizadores e atributos que indiquem o tipo de comportamento da E/S que o kernel deve apresentar. Nesse caso, usei a constante FILE_ATTRIBUTE_NORMAL, que indica apenas que o arquivo deve ser aberto para E/S síncrona normal. Lembre-se de chamar a função CloseHandle para liberar o bloqueio do kernel no arquivo quando tiver acabado de trabalhar com ele. Uma classe handle wrapper, como a que descrevi na minha coluna de julho de 2011, "C++ e a API do Windows" (msdn.microsoft.com/magazine/hh288076), fará o truque.

Agora posso seguir em frente e chamar a função ReadFile para ler o conteúdo do arquivo na memória:

char b[64]; DWORD c; VERIFY(ReadFile(f, b, sizeof(b), &c, nullptr)); printf("> %.*s\n", c, b);

Conforme esperado, o primeiro parâmetro especifica a identificação para o arquivo. Os próximos dois descrevem a memória na qual o conteúdo do arquivo deve ser lido. ReadFile também retornará se o número real de bytes copiados deve ter menos bytes disponíveis do que foi solicitado. O parâmetro final é usado apenas para E/S assíncrona. Eu voltarei nele daqui a pouco. Nesse exemplo simplista, eu simplesmente imprimo os caracteres que foram realmente lidos no arquivo. Naturalmente, talvez você precise chamar ReadFile várias vezes se for necessário.

Dois threads

Esse modelo de E/S é de fácil compreensão e, certamente, bastante útil para muitos programas pequenos, especialmente os que se baseiam em console. Mas ele não pode ser tão bem dimensionado. Se você precisar ler dois arquivos separados ao mesmo tempo, talvez para oferecer suporte a vários usuários, serão necessários dois threads. Sem problemas. É para isso que serve a função CreateThread. Este é um exemplo simples:

auto t = CreateThread(nullptr, 0, [] (void *) -> DWORD {   CreateFile/ReadFile/CloseHandle   return 0; }, nullptr, 0, nullptr); ASSERT(t); WaitForSingleObject(t, INFINITE);

Aqui estou usando um lambda sem monitoração de estado no lugar de uma função de retorno de chamada para representar o procedimento do thread. O compilador do Visual C++ 2012 está em conformidade com a especificação de linguagem do C++11 na qual lambdas sem monitoração de estado devem ser implicitamente conversíveis em ponteiros de função. Isso é conveniente, e o compilador do Visual C++ o torna melhor ao produzir automaticamente a convenção de chamada apropriada na arquitetura x86, que ostenta uma diversidade de convenções de chamada.

A função CreateThread retorna uma identificação que representa um thread que eu atendo usando a função WaitForSingleObject. O thread em si é bloqueado enquanto o arquivo é lido. Dessa maneira, posso ter vários threads executando diferentes operações de E/S em conjunto. Eu poderia então chamar WaitForMultipleObjects para aguardar até todos os threads tenham sido finalizados. Lembre-se também de chamar CloseHandle para liberar os recursos relacionados ao thread no kernel.

No entanto, essa técnica não pode ser dimensionada para além de um número limitado de usuários ou arquivos, ou seja qual for o vetor de escalabilidade para seu programa. Para ser claro, não é que várias operações de leitura pendentes não possam ser dimensionadas. Muito pelo contrário. Mas a sobrecarga do threading e da sincronização eliminarão a escalabilidade do programa.

De volta a um thread

Uma solução para esse problema é usar algo chamado E/S alertável por meio de APCs (chamadas de procedimento assíncronas). Nesse modelo, seu programa depende de uma fila de APCs em que o kernel se associa a cada thread. As APCs se apresentam nas variedades de modo de usuário e de kernel. Isto é, o procedimento, ou a função, que é enfileirado pode pertencer a um programa no modo de usuário ou, até mesmo, a algum driver no modo de kernel. O último é uma maneira simples de o kernel permitir que um driver execute algum código no contexto de um espaço de endereço do modo de usuário do thread, de modo que ele tenha acesso à sua memória virtual. Mas o truque também está disponível para programadores do modo de usuário. De qualquer forma, como a E/S é essencialmente assíncrona no hardware (e, portanto, no kernel), faz sentido começar a ler o conteúdo do arquivo e fazer com que o kernel enfileire uma APC quando ela finalmente for concluída.

Antes de mais nada, os sinalizadores e atributos passados para a função CreateFile devem ser atualizados para permitir que o arquivo forneça E/S sobreposta para que as operações no arquivo não sejam serializadas pelo kernel. Os termos assíncrono e sobreposto são usados alternadamente na API do Windows e significam a mesma coisa. De qualquer forma, a constante FILE_FLAG_OVERLAPPED deve ser usada na criação do identificador de arquivo:

auto f = CreateFile(fn, GENERIC_READ, FILE_SHARE_READ, nullptr,   OPEN_EXISTING, FILE_FLAG_OVERLAPPED, nullptr);

Novamente, a única diferença nesse trecho de código é que substituí a constante FILE_ATTRIBUTE_NORMAL pela FILE_FLAG_OVERLAPPED, mas a diferença no tempo de execução é enorme. Para fornecer de fato uma APC que o kernel possa enfileirar na conclusão da E/S, preciso usar a função alternativa ReadFileEx. Embora ReadFile possa ser usada para iniciar a E/S assíncrona, somente ReadFileEx permite que você forneça uma APC a ser chamada quando ela for concluída. Desse modo, o thread pode prosseguir e executar outros trabalhos úteis, talvez começar operações adicionais assíncronas, enquanto a E/S é concluída em segundo plano.

Novamente, graças ao C++11 e ao Visual C++, um lambda pode ser usado para representar a APC. O truque é que a APC provavelmente desejará acessar o buffer recentemente populado, mas esse não é um dos parâmetros para a APC e, como somente lambdas sem monitoração de estado são permitidos, não é possível usar o lambda para capturar a variável do buffer. A solução é, supostamente, ignorar o buffer da estrutura OVERLAPPED. Como um ponteiro para a estrutura OVERLAPPED está disponível para a APC, você poderá simplesmente converter o resultado em uma estrutura de sua escolha. A Figura 1 oferece um exemplo simples.

Figura 1 E/S alertável com APCs

struct overlapped_buffer {   OVERLAPPED o;   char b[64]; }; overlapped_buffer ob = {}; VERIFY(ReadFileEx(f, ob.b, sizeof(ob.b), &ob.o, [] (DWORD e, DWORD c,   OVERLAPPED * o) {   ASSERT(ERROR_SUCCESS == e);   auto ob = reinterpret_cast<overlapped_buffer *>(o);   printf("> %.*s\n", c, ob->b); })); SleepEx(INFINITE, true);

Além do ponteiro OVERLAPPED, a APC também forneceu um código de erro como seu primeiro parâmetro e o número de bytes copiados como seu segundo. Em algum ponto, a E/S é concluída, mas para que a APC seja executada, o mesmo thread deve ser colocado em um estado alertável. A maneira mais simples de fazer isso é com a função SleepEx, que desperta o thread assim que uma APC é enfileirada e executa todas as APCs antes de retornar o controle. Obviamente, o thread não poderá ser totalmente suspenso se já houver APCs na fila. Você também pode verificar o valor de retorno de SleepEx para descobrir o que fez com que a função fosse retomada. Você pode usar um valor de zero em vez de INFINITE para limpar a fila da APC antes de continuar sem atraso.

No entanto, usar SleepEx não é de todo útil e pode levar, com facilidade, programadores inescrupulosos a sondar APCs, e essa não é uma boa ideia. São muitas as possibilidades de que se você estiver usando E/S assíncrona de um único thread, esse thread também seja o loop de mensagens do seu programa. De qualquer maneira, você também pode usar a função MsgWaitForMultipleObjectsEx para aguardar mais do que apenas APCs e criar um tempo de execução single-threaded mais atraente para seu programa. A possível desvantagem das APCs é que elas podem trazer alguns bugs desafiadores de reentrância. Não se esqueça disso.

Um thread por processador

À medida que encontra mais tarefas para seu programa executar, você poderá notar que o processador no qual o thread do programa é executado está ficando cada vez mais ocupado, enquanto o restante dos processadores no computador está em volta esperando para fazer algo. Embora as APCs sejam praticamente a maneira mais eficiente de executar a E/S assíncrona, elas têm a clara desvantagem de sempre serem concluídas somente no mesmo thread que iniciou a operação. O desafio é então desenvolver uma solução que possa passar a operação para todos os processadores disponíveis. Você pode conceber seu próprio design, talvez coordenar o trabalho entre vários threads com loops de mensagens alertáveis, mas nada que você possa implementar chegaria perto do absoluto desempenho e escalabilidade da porta de conclusão de E/S, em grande parte devido à sua profunda integração com diferentes partes do kernel.

Enquanto uma APC permite que as operações de E/S assíncrona sejam concluídas em um único thread, uma porta de conclusão permite que qualquer thread comece uma operação de E/S e tenha os resultados processados por um thread arbitrário. Uma porta de conclusão é um objeto de kernel que você cria antes de associá-lo a outro número de objetos de arquivo, soquetes, pipes e muito mais. A porta de conclusão expõe uma interface de enfileiramento pela qual o kernel pode enviar por push um pacote de conclusão para a fila quando a E/S é concluída e seu programa pode remover o pacote da fila em qualquer thread disponível e processá-lo conforme a necessidade. Você pode até mesmo enfileirar seus próprios pacotes de conclusão se for necessário. A principal dificuldade é contornar a confusa API. A Figura 2 mostra uma classe wrapper simples para a porta de conclusão, esclarecendo como as funções são usadas e como elas se relacionam.

Figure 2 A wrapper da porta de conclusão

class completion_port {   HANDLE h;   completion_port(completion_port const &);   completion_port & operator=(completion_port const &); public:   explicit completion_port(DWORD tc = 0) :     h(CreateIoCompletionPort(INVALID_HANDLE_VALUE, nullptr, 0, tc))   {     ASSERT(h);   }   ~completion_port()   {     VERIFY(CloseHandle(h));   }   void add_file(HANDLE f, ULONG_PTR k = 0)   {     VERIFY(CreateIoCompletionPort(f, h, k, 0));   }   void queue(DWORD c, ULONG_PTR k, OVERLAPPED * o)   {     VERIFY(PostQueuedCompletionStatus(h, c, k, o));   }   void dequeue(DWORD & c, ULONG_PTR & k, OVERLAPPED *& o)   {     VERIFY(GetQueuedCompletionStatus(h, &c, &k, &o, INFINITE));   } };

A principal confusão gira em torno da tarefa dupla que a função Create­IoCompletionPort executa, primeiro criando um objeto de porta de conclusão e depois associando-o a um objeto de arquivo sobreposto. A porta de conclusão é criada uma vez e, em seguida, associada a qualquer número de arquivos. Tecnicamente, você pode executar ambas as etapas em uma única chamada, mas isso é útil apenas se você usar a porta de conclusão com um único arquivo. E onde está a graça disso?

Ao criar a porta de conclusão, a única consideração é o último parâmetro indicando a contagem do thread. Esse é o número máximo de threads que terão permissão para remover pacotes de conclusão da fila simultaneamente. Definir esse número para zero significa que o kernel permitirá um thread por processador.

A adição de um arquivo é tecnicamente chamada de associação; o principal a ser observado é o parâmetro que indica a chave a ser associada ao arquivo. Uma vez que não é possível ignorar informações extras no fim de um identificador, assim como você pode fazer com uma estrutura OVERLAPPED, a chave é uma maneira de associar algumas informações específicas do programa ao arquivo. Sempre que o kernel enfileirar um pacote de conclusão relacionado a esse arquivo, essa chave também será incluída. Isso é particularmente importante, pois o identificador de arquivo não é incluído no pacote de conclusão.

Como disse anteriormente, você pode enfileirar seus próprios pacotes de conclusão. Nesse caso, os valores que você fornece são inteiramente de sua responsabilidade. O kernel não se importa e não tentará interpretá-los de forma alguma. Desse forma, você pode fornecer um ponteiro OVERLAPPED falso e o mesmo exato endereço será armazenado no pacote de conclusão.

No entanto, em muitos casos, você aguardará que o kernel enfileire o pacote de conclusão assim que uma operação de E/S assíncrona for concluída. Normalmente, um programa cria um ou mais threads por processador e chama GetQueuedCompletionStatus, ou a função my dequeue wrapper, em um loop sem fim. Você pode enfileirar um pacote de conclusão de controle especial (um por thread) quando seu programa precisar ser finalizado e você desejar que esses threads sejam encerrados. Assim como nas APCs, você pode ignorar mais informações da estrutura OVERLAPPED para associar informações extras a cada operação de E/S:

completion_port p; p.add_file(f); overlapped_buffer ob = {}; ReadFile(f, ob.b, sizeof(ob.b), nullptr, &ob.o);

Aqui, novamente estou usando a função original ReadFile, mas nesse caso estou fornecendo um ponteiro para a estrutura OVERLAPPED como seu último parâmetro. Um thread em espera pode remover o pacote de conclusão da fila, como se segue:

DWORD c; ULONG_PTR k; OVERLAPPED * o; p.dequeue(c, k, o); auto ob = reinterpret_cast<overlapped_buffer *>(o);

Um pool de threads

Se você vem seguindo minha coluna há algum tempo, você se lembrará que nos últimos cinco meses do último ano passei falando sobre o pool de threads do Windows, em detalhes. Também não será surpresa para você que essa mesma API do pool de threads seja implementada usando portas de conclusão de E/S, fornecendo esse mesmo modelo de enfileiramento de trabalho, mas sem a necessidade de você mesmo ter que gerenciar os threads. Ele também fornece um host de recursos e conveniências que a tornam uma alternativa atraente para usar o objeto de porta de conclusão diretamente. Se ainda não fez isso, recomendo que você leia essas colunas para se inteirar sobre a API do pool de threads do Windows. Uma lista de minhas colunas online está disponível em bit.ly/StHJtH.

No mínimo, você pode usar a função TrySubmitThreadpoolCallback para obter o pool de threads a fim de criar um de seus objetos de trabalho internamente e ter o retorno de chamada imediatamente enviado para execução. Não poderia ser mais simples do que isto:

TrySubmitThreadpoolCallback([](PTP_CALLBACK_INSTANCE, void *) {   // Work goes here! }, nullptr, nullptr);

Se precisar de um pouco mais de controle, certamente você poderá criar um objeto de trabalho diretamente e associá-lo a um ambiente de pool de threads e a um grupo de limpeza. Isso também lhe proporcionará o melhor desempenho possível.

Obviamente, esta discussão é sobre E/S sobreposta e o pool de threads fornece objetos de E/S justamente para isso. Não falarei muito sobre isso, pois eu já abordei esse assunto em detalhes na minha coluna de dezembro de 2011, "Temporizadores de pool de threads e E/S" (msdn.microsoft.com/magazine/hh580731), mas a Figura 3 fornece um novo exemplo.

Figura 3 E/S de pool de threads

OVERLAPPED o = {}; char b[64]; auto io = CreateThreadpoolIo(f, [] (PTP_CALLBACK_INSTANCE, void * b,   void *, ULONG e, ULONG_PTR c, PTP_IO) {   ASSERT(ERROR_SUCCESS == e);   printf("> %.*s\n", c, static_cast<char *>(b)); }, b, nullptr); ASSERT(io); StartThreadpoolIo(io); auto r = ReadFile(f, b, sizeof(b), nullptr, &o); if (!r && ERROR_IO_PENDING != GetLastError()) {   CancelThreadpoolIo(io); } WaitForThreadpoolIoCallbacks(io, false); CloseThreadpoolIo(io);

Uma vez que CreateThreadpoolIo permite que eu passe um parâmetro de contexto adicional ao retorno de chamada em fila, não preciso ignorar o buffer da estrutura OVERLAPPED, embora eu certamente possa fazer isso se for necessário. O que não se deve esquecer aqui é que StartThreadpoolIo deve ser chamado antes do início da operação de E/S assíncrona e CancelThreadpoolIo deve ser supostamente chamada se a operação de E/S falhar ou for concluída em linha.

Threads rápidos e fluidos

Elevando o conceito de um pool de threads a novos patamares, a nova API do Windows para aplicativos da Windows Store também fornece uma abstração de pool de threads, embora uma bem mais simples, com bem menos recursos. Felizmente, nada impede que você use um pool de threads alternativo apropriado em seu compilador e suas bibliotecas. Se você vai superar os amigáveis administradores da Windows Store, isso é outra história. Ainda assim, vale mencionar o pool de threads para aplicativos da Windows Store, que integra o padrão assíncrono incorporado pela API do Windows para aplicativos da Windows Store.

Usar as inteligentes extensões do C++/CX fornece uma API relativamente simples para execução de algum código de modo assíncrono:

ThreadPool::RunAsync(ref new WorkItemHandler([] (IAsyncAction ^) {   // Work goes here! }));

Sintaticamente, isso é bastante simples e direto. Podemos até mesmo esperar que isso se torne mais simples em uma versão futura do Visual C++, caso o compilador possa gerar automaticamente um representante do C++/CX de um lambda (pelo menos conceitualmente) da mesma forma que hoje é feito para ponteiros de função.

Apesar disso, essa sintaxe relativamente simples esconde muita complexidade. Em um nível alto, ThreadPool é uma classe estática, se apropriando de um termo da linguagem C# e, portanto, não pode ser criada. Ela fornece algumas sobrecargas do método RunAsync estático. E é isso aí. Cada uma assume pelo menos um representante como seu primeiro parâmetro. Aqui, estou construindo o representante com um lambda. Os métodos RunAsync também retornam uma interface IAsyncAction, fornecendo acesso à operação assíncrona.

Convenientemente, isso funciona bem e se integra habilmente ao modelo de programação assíncrona que impregna a API do Windows para aplicativos da Windows Store. Você pode, por exemplo, encapsular a interface IAsyncAction retornada pelo método RunAsync em uma tarefa PPL (Parallel Patterns Library) e atingir um nível de capacidade de combinação semelhante ao que descrevi nas minhas colunas de setembro e outubro, "A busca por sistemas assíncronos eficientes e combináveis" (msdn.microsoft.com/magazine/jj618294) e "De volta para o futuro com funções retornáveis" (msdn.microsoft.com/magazine/jj658968).

No entanto, é útil e, de certa forma, sensato perceber o que esse código aparentemente inocente representa de fato. No centro das extensões do C++/CX existe um tempo de execução baseado em COM e na sua interface IUnknown. Como um objeto baseado em interface, possivelmente, o modelo não possa fornecer métodos estáticos. Deverá haver um objeto para que haja uma interface, e algum tipo de fábrica de classes para criar esse objeto e, de fato, existe.

O Tempo de Execução do Windows define algo chamado de classe de tempo de execução que é muito mais parecido com uma classe COM tradicional. Se você for conservador, poderá até mesmo definir a classe em um arquivo IDL e executá-la por meio de uma nova versão do compilador MIDL, especificamente adequado para a tarefa, e ela vai gerar os arquivos de metadados .winmd e os cabeçalhos apropriados.

Um classe de tempo de execução apresenta métodos de instância e métodos estáticos. Eles são definidos com interfaces distintas. A interface contendo os métodos de instância se torna a interface padrão da classe e a interface contendo os métodos estáticos é atribuída à classe de tempo de execução nos metadados gerados. Nesse caso, a classe de tempo de execução ThreadPool carece do atributo activatable e não tem interface padrão, mas uma vez criada, a interface estática pode ser consultada e, assim, métodos não tão estáticos podem ser chamados. A Figura 4 fornece um exemplo do que isso pode envolver. Lembre-se de que a maioria disso poderia ser gerada pelo compilador, mas deve dar uma boa ideia do quanto realmente custa fazer com que esse método estático simples seja chamado para executar um representante de modo assíncrono.

Figura 4 O pool de threads do WinRT

class WorkItemHandler :   public RuntimeClass<RuntimeClassFlags<ClassicCom>,   IWorkItemHandler> {   virtual HRESULT __stdcall Invoke(IAsyncAction *)   {     // Work goes here!     return S_OK;   } }; auto handler = Make<WorkItemHandler>(); HSTRING_HEADER header; HSTRING clsid; auto hr = WindowsCreateStringReference(   RuntimeClass_Windows_System_Threading_ThreadPool,    _countof(RuntimeClass_Windows_System_Threading_ThreadPool)   - 1, &header, &clsid); ASSERT(S_OK == hr); ComPtr<IThreadPoolStatics> tp; hr = RoGetActivationFactory(   clsid, __uuidof(IThreadPoolStatics),   reinterpret_cast<void **>(tp.GetAddressOf())); ASSERT(S_OK == hr); ComPtr<IAsyncAction> a; hr = tp->RunAsync(handler.Get(), a.GetAddressOf()); ASSERT(S_OK == hr);

Certamente, isso está a uma longa distância da simplicidade e eficiência relativas de chamar a função TrySubmitThreadpoolCallback. É útil entender o custo das abstrações que você usa, mesmo que acabe decidindo que o custo se justifica devido a alguma medida de produtividade. Deixe-me analisar isso rapidamente.

O representante WorkItemHandler é, de fato, uma interface IWorkItemHandler baseada em IUnknown com um único método Invoke. A implementação dessa interface não é fornecida pela API, mas pelo compilador. Isso faz sentido, pois ela fornece um contêiner conveniente para todas as variáveis capturadas pelo lambda e o corpo do lambda residiria, naturalmente, no método Invoke gerado pelo compilador. Nesse exemplo, simplesmente dependo da classe de modelo RuntimeClass da WRL (Biblioteca de Tempo de Execução do Windows) para implementar IUnknown para mim. Assim, posso usar a útil função de modelo Make para criar uma instância de meu WorkItemHandler. Para lambdas sem monitoração de estado e ponteiros de funções, eu ainda esperaria o compilador gerar uma implementação estática com uma implementação não operacional de IUnknown para evitar a sobrecarga de alocação dinâmica.

Para criar uma instância da classe de tempo de execução, preciso chamar a função RoGet­ActivationFactory. No entanto, ela precisa de uma ID de classe. Observe que essa não é CLSID do COM tradicional, mas o nome totalmente qualificado do tipo, nesse caso, Windows.System.Threading.ThreadPool. Aqui estou usando uma matriz constante gerada pelo compilador MIDL para evitar ter que contar a cadeia de caracteres em tempo de execução. Como se isso não fosse suficiente, também preciso criar uma versão HSTRING dessa ID de classe. Aqui estou usando a função WindowsCreateStringReference que, diferentemente da função WindowsCreateString regular, não cria uma cópia da cadeia de caracteres de origem. Por conveniência, a WRL também fornece a classe HStringReference, que encapsula essa funcionalidade. Agora posso chamar a função RoGetActivationFactory, solicitando a interface IThreadPoolStatics diretamente e armazenando o ponteiro resultante em um ponteiro inteligente fornecido pela WRL.

Agora posso, finalmente, chamar o método RunAsync nessa interface, fornecendo a ela minha implementação de IWorkItemHandler, bem como o endereço de um ponteiro inteligente IAsyncAction representando o objeto de ação resultante.

Desse modo, talvez não seja surpresa que essa API de pool de threads não forneça nada próximo da quantidade de funcionalidade e flexibilidade fornecidas pela principal API de pool de threads do Windows ou pelo Tempo de Execução Simultâneo. No entanto, a vantagem do C++/CX e das classes de tempo de execução é percebida pelos limites entre o programa e o tempo de execução em si. Como um programador do C++, você pode estar agradecido que o Windows 8 não seja uma plataforma inteiramente nova e que a API tradicional do Windows ainda esteja à sua disposição, se e quando você precisar dela.

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

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: James P. McNellis