Junho de 2018

Volume 33 Número 6

C++ — Assíncrono Efetivo com Corrotinas e C++/WinRT

Por Kenny Kerr | De 2018 junho

O tempo de execução do Windows tem um modelo assíncrono relativamente simples no sentido de que, como tudo no Windows Runtime, ele se concentra na permitindo que os componentes para expor os métodos assíncronos e tornando mais fácil para aplicativos chamar esses métodos assíncronos. Ele não no próprio fornece um tempo de execução de simultaneidade ou até mesmo nada no caminho de blocos de construção para produzir ou consumir métodos assíncronos. Em vez disso, tudo isso é da esquerda para as projeções de idiomas individuais. Isso é como ele deve ser e não se destina a trivializam o padrão assíncrono de tempo de execução do Windows. É nada simples de implementar esse padrão corretamente. Obviamente, isso também significa que percepção do desenvolvedor de async no Windows Runtime muito é influenciada por opção de idioma do desenvolvedor. Um desenvolvedor que usou só C + + CX pode, por exemplo, incorretamente, mas é compreensível supõem que async é uma mensagem ativa.

A estrutura de simultaneidade ideal para o desenvolvedor c# será diferente da biblioteca de simultaneidade ideal para o desenvolvedor de C++. A função de linguagem e bibliotecas no caso do C++, é cuidar dos mecanismos do padrão assíncrono e fornecer uma ponte natural para uma implementação de idioma específico.

Co-rotinas são a abstração preferencial para implementar e chamar os métodos assíncronos em C++, mas vamos primeiro certificar-se de que entender como funciona o modelo assíncrono. Considere uma classe com um único método estático que tem a seguinte aparência:

struct Sample
{
  Sample() = delete;
  static Windows::Foundation::IAsyncAction CopyAsync();
};

Os métodos assíncronos terminam com "Async" por convenção, portanto você pode pensar nisso como o método de cópia assíncrona. Pode haver uma alternativa de bloqueio ou síncrona é simplesmente chamada de cópia. É possíveis que um chamador pode querer um método de cópia de bloqueio para uso por um thread em segundo plano e um método sem bloqueio ou assíncrono, para uso por um thread de interface do usuário que não pode bloquear por medo aparecendo não responder.

Primeiro, o método CopyAsync pode parecer muito simple chamar. Eu poderia escrever o código C++ a seguir:

IAsyncAction async = Sample::CopyAsync();

Como você pode imaginar, a IAsyncAction resultante não é realmente o resultado final do método assíncrono, mesmo que seja o resultado de chamar o método CopyAsync de maneira tradicional de procedimento. O IAsyncAction é o objeto que o chamador pode usar a esperar após o resultado de forma síncrona ou assíncrona, dependendo da situação. Juntamente com IAsyncAction, há três outras interfaces bem conhecidos que sigam um padrão semelhante e oferecem recursos diferentes para o receptor comunicar informações de volta para o chamador. A tabela Figura 1 fornece uma comparação das quatro interfaces assíncronas.

Figura 1 um comparação das Interfaces assíncronas

Name Result progresso
IAsyncAction Não Não
IAsyncActionWithProgress Não Sim
IAsyncOperation Sim Não
IAsyncOperationWithProgress Sim Sim

Em termos de C++, as interfaces podem ser expressos como mostrado na Figura 2.

Figura 2 as Interfaces assíncronas expressadas em termos de C++

namespace Windows::Foundation
{
  struct IAsyncAction;
  template <typename Progress>
  struct IAsyncActionWithProgress;
  template <typename Result>
  struct IAsyncOperation;
  template <typename Result, typename Progress>
  struct IAsyncOperationWithProgress;
}

IAsyncAction e IAsyncActionWithProgress podem ser aguardada para determinar quando o método assíncrono é concluído, mas essas interfaces não oferece qualquer resultado observável ou valor de retorno diretamente. IAsyncOperation e IAsyncOperationWithProgress, por outro lado, esperar que o parâmetro de tipo de resultado para indicar o tipo de resultado que pode ser esperado quando o método assíncrono é concluído com êxito. Por fim, IAsyncActionWithProgress e IAsyncOperationWithProgress esperar que o parâmetro de tipo de andamento para indicar o tipo de informações de progresso que podem ser esperadas periodicamente para operações de longa execução até que o método assíncrono é concluído.

Há algumas maneiras de aguardar após o resultado de um método assíncrono. Eu não descrevem todas aqui que seriam transformar isso em um artigo muito longo. Embora haja várias maneiras para lidar com conclusão assíncrona, há apenas duas recomendo: o método async.get, que executa uma espera de bloqueio, e a expressão co_await assíncrono, que realiza uma espera cooperativa no contexto de uma corrotina. Não é melhor que o outro como eles simplesmente finalidades diferentes. Vejamos agora como fazer uma espera de bloqueio.

Como já mencionei, uma espera de bloqueio pode ser obtida usando o método get da seguinte maneira:

IAsyncAction async = Sample::CopyAsync();
async.get();

Raramente houver qualquer valor em manter o objeto assíncrono e o seguinte formato, portanto, é preferível:

Sample::CopyAsync().get();

É importante ter em mente que o método get bloqueará o thread de chamada até que o método assíncrono é concluído. Como tal, não é apropriado usar o método get em um thread de interface do usuário porque ele pode fazer com que o aplicativo pare de responder. Uma declaração será acionado em compilações não otimizadas se você tentar fazê-lo. O método get é ideal para aplicativos de console ou threads em segundo plano em que, por algum motivo, não convém usar uma corrotina.

Depois do método assíncrono é concluído, o método get retornará um resultado diretamente para o chamador. No caso de IAsyncAction e IAsyncActionWithProgress, o tipo de retorno será nulo. Que podem ser úteis para um método assíncrono que inicia uma operação de cópia de arquivo, mas menos tão para algo como um método assíncrono que lê o conteúdo de um arquivo. Vamos adicionar outro método assíncrono para o exemplo:

struct Sample
{
  Sample() = delete;
  static Windows::Foundation::IAsyncAction CopyAsync();
  static Windows::Foundation::IAsyncOperation<hstring> ReadAsync();
};

No caso de ReadAsync, o método get corretamente encaminhará o resultado de hstring para o chamador após a conclusão da operação:

Sample::CopyAsync().get();
hstring result = Sample::ReadAsync().get();

Supondo que retorna a execução do método get, a cadeia de caracteres resultante conterá o valor que foi retornado pelo método assíncrono após sua conclusão bem-sucedida. Execução pode não retornar, por exemplo, se ocorrer um erro.

O método get é limitado no sentido de que ele não pode ser usado em um thread de interface do usuário, nem-lo a explorar todo o potencial de simultaneidade do computador, uma vez que retém a manter de reféns chamada do thread até que o método assíncrono é concluído. Usar uma corrotina permite que o método assíncrono concluído sem manter um recurso precioso cativo por algum tempo indeterminado.

Tratamento de conclusão assíncrona

Agora que você tem um identificador em interfaces assíncronas em geral, vamos começar a fazer drill down em como elas funcionam em mais detalhes. Supondo que você não estiver satisfeito com a espera de bloqueio fornecida pelo método get, o que outras opções há? É assim alternará engrenagens e co-rotinas inteiramente em foco, mas, por enquanto, vamos examinar mais detalhadamente essas interfaces assíncronas para ver o que eles oferecem. Tanto o suporte de corrotina, bem como o método get que procuramos anteriormente, depende de contrato e estado indicado por essas interfaces no computador. Não entrarei em muitos detalhes porque você não precisa saber tudo o que muito sobre eles, mas vamos explorar as Noções básicas pelo menos deve estar familiarizados se você tiver entre aqui e usá-los diretamente para algo mais fora do comum.

Todos os quatro das interfaces assíncronas logicamente derivam da interface IAsyncInfo. Há muito pouco, você pode fazer com IAsyncInfo e é encaramos se ele ainda existe porque ele adiciona um pouco de sobrecarga. Os únicos membros IAsyncInfo que considere realmente são Status, que pode informar se o método assíncrono foi concluída, e Cancelar, o que pode ser usado para solicitar o cancelamento de uma operação de longa execução cujo resultado não é mais necessário. Eu nitpick esse design porque eu realmente como o padrão assíncrono, em geral e deseja apenas fosse perfeito porque ele é tão muito perto.

O membro de Status pode ser útil se você precisar determinar se um método assíncrono foi concluída sem realmente está aguardando a ele. Veja um exemplo:

auto async = ReadAsync();
if (async.Status() == AsyncStatus::Completed)
{
  auto result = async.GetResults();
  printf("%ls\n", result.c_str());
}

Cada uma das quatro interfaces assíncronas, não IAsyncInfo em si, fornece versões individuais do método GetResults que deve ser chamado somente depois de determinar que o método assíncrono foi concluída. Não confunda isso com o método get fornecido pelo C + + WinRT. Enquanto GetResults é implementada pelo próprio método assíncrono, get é implementado pelo C + + WinRT. GetResults não bloquearão se o método assíncrono ainda está em execução e provavelmente lançará uma exceção hresult_illegal_method_call se for chamado prematuramente. Sem dúvida, pode começar a imaginar como o método de obter bloqueio é implementado. Conceitualmente, ele pode ter esta aparência:

auto get() const
{
  if (Status() != AsyncStatus::Completed)
  {
    // Wait for completion somehow ...
  }
  return GetResults();
}

A implementação real é um pouco mais complicada, mas isso captura a essência do mesmo. O ponto aqui é que é chamada de GetResults independentemente de ser um IAsyncOperation, que retorna um valor, ou IAsyncAction, que não. O motivo disso é que GetResults é responsável para propagação de quaisquer erros que possam ter ocorrido dentro da implementação do método assíncrono e serão relançar a exceção conforme necessário.

A pergunta que permanece é como o chamador pode aguardar a conclusão. Vou escrever uma função de membro não get para mostrar o que está envolvido. Vou começar com essa estrutura básica inspirada pelo método get conceitual anterior:

template <typename T>
auto get(T const& async)
{
  if (async.Status() != AsyncStatus::Completed)
  {
    // Wait for completion somehow ...
  }
  return async.GetResults();
}

Desejo esse modelo de função para trabalhar com todos os quatro interfaces assíncronas, portanto, vou usar a instrução return forma unilateral. Provisionar especial é feita na linguagem do C++ para genericity e pode ser agradecer para que.

Cada uma das interfaces assíncronas quatro fornece um membro concluído exclusivo que pode ser usado para registrar um retorno de chamada, chamado de delegado, que será chamado quando o método assíncrono é concluído. Na maioria dos casos, C + + WinRT criará automaticamente o representante para você. Tudo o que você precisa fazer é fornecer algum manipulador de tipo função e uma expressão lambda é geralmente mais simples:

async.Completed([](auto&& async, AsyncStatus status)
{
  // It's done!
});

O tipo de delegado parâmetro da primeira será que, da assíncrona de interface que acabou de concluir, mas tenha em mente que o preenchimento deve ser considerado como um sinal simple. Em outras palavras, não colocar um grupo de código dentro do manipulador concluída. Essencialmente, você deve considerá-la como um manipulador de noexcept porque o método assíncrono não se sabe o que fazer com todas as falhas que ocorrem dentro deste manipulador. Então, o que você pode fazer?

Bem, você pode simplesmente notificar um thread de espera usando um evento. Figura 3 mostra a aparência a função get.

Figura 3 notificando um espera usando um evento de Thread

template <typename T>
auto get(T const& async)
{
  if (async.Status() != AsyncStatus::Completed)
  {
    handle signal = CreateEvent(nullptr, true, false, nullptr);
    async.Completed([&](auto&&, auto&&)
    {
      SetEvent(signal.get());
    });
    WaitForSingleObject(signal.get(), INFINITE);
  }
  return async.GetResults();
}

C + + WinRT obter métodos use uma variável de condição com um bloqueio de leitor/gravador porque ele é um pouco mais eficiente. Um tipo de variant algo parecido com o que é mostrado na Figura 4.

Figura 4 usando uma variável de condição com um bloqueio de leitor/gravador

template <typename T>
auto get(T const& async)
{
  if (async.Status() != AsyncStatus::Completed)
  {
    slim_mutex m;
    slim_condition_variable cv;
    bool completed = false;
    async.Completed([&](auto&&, auto&&)
    {
      {
        slim_lock_guard const guard(m);
        completed = true;
      }
      cv.notify_one();
    });
    slim_lock_guard guard(m);
    cv.wait(m, [&] { return completed; });
  }
  return async.GetResults();
}

Obviamente, você pode usar o mutex da biblioteca padrão C++ e a variável de condição se você preferir. O ponto aqui é simplesmente que o manipulador concluído é o gancho para cabeamento de conclusão assíncrona e pode ser feito muito genericamente.

Naturalmente, não há nenhum motivo para escrever sua própria função get e co-rotinas mais do que provável serão muito mais simples e mais versáteis em geral. Ainda assim, esperamos que isso ajuda você a avaliar alguns o poder e flexibilidade no tempo de execução do Windows.

Gera objetos assíncrono

Agora que depois de explorar as interfaces assíncronas e alguns mecanismos de conclusão em geral, vamos examinar a criação ou a produção de implementações dessas interfaces assíncronas quatro. Como você já aprendeu, implementando interfaces WinRT com C + + WinRT é muito simple. Pode, por exemplo, implementar IAsyncAction, conforme mostrado no Figura 5.

Figura 5 implementando IAsyncAction

struct MyAsync : implements<MyAsync, IAsyncAction, IAsyncInfo>
{
  // IAsyncInfo members ...
  uint32_t Id() const;
  AsyncStatus Status() const;
  HRESULT ErrorCode() const;
  void Cancel() const;
  void Close() const;
  // IAsyncAction members ...
  void Completed(AsyncActionCompletedHandler const& handler) const;
  AsyncActionCompletedHandler Completed() const;
  void GetResults() const;
};

A dificuldade é quando você pensar em como você pode implementar esses métodos. Embora não seja difícil imaginar algumas implementação, é quase impossível fazer neste corretamente sem primeiro engenharia como as projeções de idioma existente, na verdade, implementação-las. Você pode ver, o padrão assíncrono WinRT só funciona se todos implementa essas interfaces usando uma máquina de estado muito específicas exatamente da mesma maneira. Cada projeção de idioma faz as mesmas suposições sobre como essa máquina de estado é implementada e se você se deparar com implementá-lo de forma ligeiramente diferente, coisas ruins ocorrerá.

Felizmente, você não precisa se preocupar sobre isso, porque cada projeção de idioma, com exceção do C + + CX, já implementa corretamente para você. Aqui está uma implementação completa de IAsyncAction graças C + + suporte de corrotina do WinRT:

IAsyncAction CopyAsync()
{
  co_return;
}

Agora, isso não é uma implementação muito interessante, mas é muito educacional e um bom exemplo de apenas quanto C + + WinRT está fazendo para você. Como esta é uma implementação completa, podemos usá-lo para exercitar algumas do que aprendemos até o momento. A função CopyAsync anterior é uma corrotina. Tipo de retorno de corrotina é usado para compor uma implementação de IAsyncAction e IAsyncInfo, e o compilador do C++ proporciona vida no momento certo. Podemos vai explorar alguns dos detalhes mais tarde, mas agora vamos observar como desta corrotina funciona. Considere o seguinte aplicativo de console:

IAsyncAction CopyAsync()
{
  co_return;
}
int main()
{
  IAsyncAction async = CopyAsync();
  async.get();
}

A principal função chama a função CopyAsync, que retorna um IasyncAction. Se você esquecer por um momento que o CopyAsync corpo da função ou a aparência da definição, ele deve ser evidente que se trata de apenas uma função que retorna um objeto IAsyncAction. Portanto, pode ser usada em todas as maneiras que você já aprendeu.

Uma corrotina (desse tipo) deve ter uma instrução de co_return ou co_await. Ele pode, obviamente, ter várias instruções, mas deve ter pelo menos um deles para ser uma corrotina. Como esperado, uma instrução co_return não apresentam nenhum tipo de suspensão ou assincronia. Portanto, essa função CopyAsync produz um IAsyncAction que é concluído imediatamente ou síncrona. Eu pode ilustrar isso da seguinte maneira:

IAsyncAction Async()
{
  co_return;
}
int main()
{
  IAsyncAction async = Async();
  assert(async.Status() == AsyncStatus::Completed);
}

A asserção é garantida como true. Não há nenhuma corrida aqui. Como CopyAsync é apenas uma função, o chamador é bloqueado até que ele retorna e a primeira oportunidade para que ela retorne é a instrução co_return. Isso significa que se você tiver algum contrato de async que você precisa implementar, mas a implementação, na verdade, não precisa apresentar qualquer assincronia, ele pode simplesmente retornar o valor diretamente e sem bloqueio ou apresentando uma alternância de contexto. Considere uma função que baixa e, em seguida, retorna um valor armazenado em cache, conforme mostrado no Figura 6.

Função Figura 6 que baixa e retorna um valor armazenado em cache

hstring m_cache;
IAsyncOperation<hstring> ReadAsync()
{
  if (m_cache.empty())
  {
    // Download and cache value ...
  }
  co_return m_cache;
}
int main()
{
  hstring message = ReadAsync().get();
  printf("%ls\n", message.c_str());
}

Na primeira vez que ReadAsync é chamado, o cache é provavelmente vazio e o resultado é baixado. Provavelmente isso suspenderá a corrotina próprio enquanto isso ocorre. Suspensão implica que a execução retorna ao chamador. O chamador é entregue a um objeto de async que não foi, na verdade, concluído, portanto, a necessidade de aguardar a conclusão de alguma forma.

A vantagem de co-rotinas é que há uma única abstração para produzir objetos async e para consumir esses mesmos objetos assíncrono. Uma API ou autor de componente pode implementar um método assíncrono, conforme descrito anteriormente, mas um consumidor de API ou o desenvolvedor do aplicativo também pode usar co-rotinas para chamar e aguarda sua conclusão. Vamos agora reescrever a principal função do Figura 6 usar uma corrotina para fazer a espera:

IAsyncAction MainAsync()
{
  hstring result = co_await ReadAsync();
  printf("%ls\n", result.c_str());
}
int main()
{
  MainAsync().get();
}

Tenho essencialmente feito o corpo da função principal antigo e movê-la para a corrotina MainAsync. A função principal usa o método get para impedir que o aplicativo Finalizando a corrotina seja concluído assincronamente. A função MainAsync tem algo novo e que a instrução co_await. Em vez de usar o método get para bloquear o thread de chamada até que ReadAsync for concluída, a instrução de co_await é usada para aguardar a função ReadAsync concluir de forma cooperativa ou sem bloqueio. Isso é o que eu quis por um ponto de suspensão. A instrução co_await representa um ponto de suspensão. Este aplicativo só chama ReadAsync uma vez, mas você pode imaginar que está sendo chamada várias vezes em um aplicativo mais interessante. Na primeira vez que ele é chamado, a corrotina MainAsync será realmente suspender e devolver o controle ao chamador. Na segunda vez que ele é chamado, ele não suspender em todos os mas retorna o valor diretamente.

Co-rotinas são muito novas para muitos desenvolvedores do C++, para que não se sentir incorreta se isso ainda parece mágico em vez disso. Esses conceitos ficará muito claras que você começa a criação e depuração co-rotinas por conta própria. A boa notícia é que você já sabe o suficiente para começar a fazer uso efetivo do co-rotinas consumir async APIs fornecidas pelo Windows. Por exemplo, você deve ser possível ponderar sobre como o aplicativo de console em Figura 7 funciona.

Figura 7 um aplicativo de Console de exemplo com co-rotinas

#include "winrt/Windows.Web.Syndication.h"
using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;
IAsyncAction MainAsync()
{
  Uri uri(L"https://kennykerr.ca/feed");
  SyndicationClient client;
  SyndicationFeed feed = co_await client.RetrieveFeedAsync(uri);
  for (auto&& item : feed.Items())
  {
    hstring title = item.Title().Text();
    printf("%ls\n", title.c_str());
  }
}
int main()
{
  init_apartment();
  MainAsync().get();
}

Experimente agora e ver apenas quanto fun é usar C++ moderno no Windows.

Co-rotinas e o Pool de threads

Criar uma corrotina básica é simples. Você pode facilmente co_await alguma outra ação assíncrona ou operação, simplesmente co_return um valor, ou desenvolver uma combinação dos dois. Aqui está uma corrotina que não é assíncrona em todos os:

IAsyncOperation<int> return_123()
{
  co_return 123;
}

Mesmo que ele executa de forma síncrona, ela ainda produz uma implementação válida completamente da interface IAsyncOperation:

int main()
{
  int result = return_123().get();
  assert(result == 123);
}

É aqui que irá aguardar cinco segundos antes de retornar o valor:

using namespace std::chrono;
IAsyncOperation<int> return_123_after_5s()
{
  co_await 5s;
  co_return 123;
}

O outro foram vai executar de forma assíncrona e ainda a função principal permanece praticamente inalterado, graças ao comportamento de bloqueio da função get:

int main()
{
  int result = return_123_after_5s().get();
  assert(result == 123);
}

A instrução de co_return em corrotina da último será executada no pool de threads do Windows, porque a expressão co_await for uma duração de chrono que usa um temporizador de pool de thread. A instrução co_await representa um ponto de suspensão e deve ser aparente que uma corrotina pode continuar em um thread completamente diferente depois de suspensão. Você também pode fazer nesse explícita usando resume_background:

IAsyncOperation<int> background_123()
{
  co_await resume_background();
  co_return 123;
}

Não há nenhum atraso aparente neste momento, mas tem a garantia de corrotina retomar no pool de threads. Se você não tiver certeza? Você pode ter um valor armazenado em cache e só quiser apresentar uma alternância de contexto, se o valor deve ser recuperado do armazenamento latente. Este é o que é bom Lembre-se de que uma corrotina é também uma função para que todas as regras normais se aplicam:

IAsyncOperation<int> background_123()
{
  static std::atomic<int> result{0};
  if (result == 0)
  {
    co_await resume_background();
    result = 123;
  }
  co_return result;
}

Isso só condicionalmente vai apresentar a simultaneidade. Vários threads podem perfeitamente de corrida no e chamar background_123, fazendo com que alguns para continuar no pool de threads, mas, eventualmente, a variável atômica será preparada e começará a corrotina concluir de forma síncrona. Isto é, logicamente, a pior das hipóteses.

Digamos que o valor só pode ser lidos do armazenamento quando um sinal é gerado, indicando que o valor está pronto. Podemos usar as duas co-rotinas em Figura 8 para que isso aconteça.

Figura 8 ler um valor de armazenamento depois que um sinal é gerado

handle m_signal{ CreateEvent(nullptr, true, false, nullptr) };
std::atomic<int> m_value{ 0 };
IAsyncAction prepare_result()
{
  co_await 5s;
  m_value = 123;
  SetEvent(m_signal.get());
}
IAsyncOperation<int> return_on_signal()
{
  co_await resume_on_signal(m_signal.get());
  co_return m_value;
}

A primeira corrotina artificialmente aguarda até cinco segundos, define o valor e, em seguida, sinaliza o evento de Win32. A segundo corrotina aguarda o evento fique sinalizado e, em seguida, simplesmente retorna o valor. Novamente, o pool de threads é usado para aguardar o evento, levando a uma implementação eficiente e escalonável. Coordenando as dois co-rotinas é simples:

int main()
{
  prepare_result();
  int result = return_on_signal().get();
  assert(result == 123);
}

A função principal inicia a corrotina primeiro, mas não bloqueia a aguardar sua conclusão. A segundo corrotina começa imediatamente a espera para o valor, como ele faz isso de bloqueio.

Co-rotinas e o contexto de chamada

Até agora, eu concentramos no pool de threads, ou o que pode ser chamado threads em segundo plano. C + + WinRT adora o pool de threads do Windows, mas invariavelmente você precisa fazer o trabalho de volta para um thread de primeiro plano que representa alguma interação do usuário. Vamos examinar as maneiras de fazer um controle preciso sobre o contexto de execução.

Como co-rotinas podem ser usadas para introduzir concorrência ou lidar com a latência em outras APIs, alguma confusão pode surgir como contexto de execução de uma corrotina fornecido a qualquer momento específico no tempo. Vamos apagados algumas coisas.

Figura 9 mostra uma função simple que imprimirá algumas informações básicas sobre o thread de chamada.

Figura 9 imprimir informações básicas sobre o Thread de chamada

void print_context()
{
  printf("thread:%d apartment:", GetCurrentThreadId());
  APTTYPE type;
  APTTYPEQUALIFIER qualifier;
  HRESULT const result = CoGetApartmentType(&type, &qualifier);
  if (result == S_OK)
  {
    puts(type == APTTYPE_MTA ? "MTA" : "STA");
  }
  else
  {
    puts("N/A");
  }
}

Isso não é um despejo completa ou à prova de falhas de informações de compartimento, mas é bom o bastante para nossos objetivos hoje. Para os COM nerds aí, n/d é "não aplicável" e não o outra NA você está pensando. Lembre-se de que há dois modelos de compartimento primário. Um processo tem no máximo um multi-threaded apartment (MTA) e pode ter qualquer número de single-threaded apartments (STA). Apartments são uma realidade ruim projetada para acomodar a arquitetura de comunicação remota do COM, mas tradicionalmente foram usadas para dar suporte a objetos COM que não foram thread-safe.

Para garantir que os objetos são apenas chamados do thread no qual eles são criados, STA é usado pelo COM. Naturalmente, isso indica algum mecanismo para realizar marshaling de chamadas de outros threads de volta para o thread apartment. STAs geralmente usam uma fila de loop ou dispatcher de mensagens para isso. O MTA é usado pelo COM para indicar que existe sem afinidade de thread, mas esse processo de empacotamento é necessário se origina de uma chamada em alguns outros apartment. A relação entre objetos e threads é complexa, para que será salvo para outro dia. O cenário ideal é quando um objeto dizendo que ela é agile e, portanto, livre de afinidade de apartment.

Vamos usar a função print_context para escrever alguns programas de interessantes. Aqui está um que chama print_context antes e depois de usar resume_background para mover o trabalho em um thread em segundo plano (pool de threads):

IAsyncAction Async()
{
  print_context();
  co_await resume_background();
  print_context();
}

Considere também o chamador a seguir:

int main()
{
  init_apartment();
  Async().get();
}

O chamador é importante porque ele determina o contexto original ou a chamada para a corrotina (ou qualquer função). Nesse caso, init_apartment é chamado do principal sem argumentos. Isso significa que o thread principal do aplicativo unirá o MTA. Portanto, ele não requer um loop de mensagem ou o distribuidor de qualquer tipo. Isso também significa que o thread Felizmente pode bloquear a execução como feito aqui usando a função de bloqueio de get para aguardar a corrotina concluir. Uma vez que o thread de chamada é um thread MTA, a corrotina inicia a execução no MTA. A expressão co_await resume_background é usado para suspender a corrotina momentaneamente para que o thread de chamada é liberado de volta ao chamador e a corrotina em si é gratuita continuar assim que um thread está disponível do pool de threads para que a corrotina pode Continue a executar simultaneamente. Uma vez no pool de threads (também conhecido como um thread em segundo plano), print_context novamente é chamado. Aqui está o que você pode ver se você executar este programa:

thread:18924 apartment:MTA
thread:9568 apartment:MTA

Os identificadores de thread não importam. O que importa é que eles são exclusivos. Observe que o thread temporariamente fornecido pelo pool de threads é também um MTA. Como isso pode ocorrer se eu não chamar init_apartment nesse thread? Se você entrar na função print_context, você observará que o APTTYPEQUALIFIER faz distinção entre esses threads e identifica a associação de thread como sendo implícita. Você poderá supor que um thread do pool é um thread de MTA na prática, desde que o processo está se comportando MTA atividade por outros meios.

A chave é que a expressão co_await resume_background será efetivamente alternar o contexto de execução de uma corrotina ao pool de threads, independentemente de qual thread ou apartment no qual ele foi originalmente em execução. Uma corrotina deve supor que não devem bloquear o chamador e certifique-se de que a avaliação de uma corrotina está suspenso antes de alguma operação vinculada à computação, potencialmente bloquear o thread de chamada. Isso não significa que resume_background sempre deve ser usado. Uma corrotina pode existir simplesmente para algum outro conjunto de co-rotinas de agregação. Nesse caso, qualquer expressão co_await dentro de corrotina pode fornecer a suspensão necessário para garantir que o thread de chamada não está bloqueado.

Agora, considere o que acontece se eu alterar o chamador, principal do aplicativo do função, da seguinte maneira:

int main()
{
  init_apartment(apartment_type::single_threaded);
  Async();
  MSG message;
  while (GetMessage(&message, nullptr, 0, 0))
  {
    DispatchMessage(&message);
  }
}

Isso parece bastante razoável. O thread principal se torna um thread STA, chama a função Async (sem bloqueio de qualquer maneira) e inserirá um loop de mensagem para garantir que o STA pode atender chamadas entre apartment. O problema é que o MTA ainda não foi criado, para que os threads de pool de threads não associar o MTA implicitamente e, portanto, não é possível fazer uso do COM os serviços, como a ativação. Aqui está o que você pode ver se você executar o programa agora:

thread:17552 apartment:STA
thread:19300 apartment:N/A

Observe que a suspensão de resume_background a seguir, a corrotina não tem um contexto de apartment no qual executar o código que depende do tempo de execução COM. Se você realmente precisa de um STA para o thread principal, esse problema é resolvido facilmente, garantindo que o MTA está "sempre ativado" independentemente, conforme mostrado no Figura 10.

Figura 10, garantindo o MTA é "Sempre ativos"

int main()
{
  CO_MTA_USAGE_COOKIE mta{};
  check_hresult(CoIncrementMTAUsage(&mta));
  init_apartment(apartment_type::single_threaded);
  Async();
  MSG message;
  while (GetMessage(&message, nullptr, 0, 0))
  {
    DispatchMessage(&message);
  }
}

A função CoIncrementMTAUsage garantirá que o MTA é criado. O thread de chamada se torna um membro implícito o MTA até ou a menos que uma opção explícita é feita para unir outra apartment, como é o caso aqui. Se eu executar o programa novamente, que seria obtido o resultado desejado:

thread:11276 apartment:STA
thread:9412 apartment:MTA

Isso é basicamente o ambiente no qual estão mais maioria dos aplicativos do Windows, mas agora você sabe um pouco mais sobre como você pode controlar ou criar o ambiente por conta própria. Una-me próxima vez conforme continuamos a explorar co-rotinas.

Conclusão

Bem, eu esteve muito profundo em async e co-rotinas em C++. Sempre há mais a dizer sobre simultaneidade. Esse é um tópico tal fascinante e espero que esta introdução irá proporcionar a você empolgados como é simples para lidar com async em seus aplicativos C++ e componentes e a potência simples ao seu alcance quando você começar a usar C + + WinRT.


Kenny Kerré um autor, Programador de sistemas e o criador do C + + WinRT. Ele também é um engenheiro de equipe do Windows no Microsoft onde ele está entrando desalocar o futuro do C++ para Windows, permitindo que os desenvolvedores a escrever aplicativos de alto desempenho lindos e componentes com facilidade incrível.