Linhas de execução em segundo plano nas bibliotecas de cliente do C++

Este guia descreve o modelo de linha de execução usado pelas bibliotecas de cliente C++ e mostra como modificar os pools de linhas de execução padrão no aplicativo.

Objetivos

  • Descrever o modelo de linha de execução padrão para as bibliotecas de cliente C++.
  • Descreva como substituir esses padrões nos aplicativos que precisam.

Por que as bibliotecas de cliente usam linhas de execução em segundo plano?

A maioria das funções nas bibliotecas de cliente usa a linha de execução que chama a função para concluir todo o trabalho, incluindo qualquer RPC para o serviço e/ou atualização de tokens de acesso para autenticação.

As funções assíncronas, por sua natureza, não podem usar a linha de execução atual para concluir o trabalho. Algumas linhas de execução separadas precisam aguardar a conclusão do trabalho e processar a resposta.

Também não é bom bloquear a linha de execução de chamada em operações de longa duração, em que o serviço pode levar minutos ou mais para concluir o trabalho. Para essas operações, a biblioteca de cliente usa linhas de execução em segundo plano para pesquisar periodicamente o estado da operação de longa duração.

Quais funções e bibliotecas exigem linhas de execução em segundo plano?

As funções que retornam um future<T> para algum tipo de T usam linhas de execução em segundo plano para aguardar até que o trabalho seja concluído.

Nem todas as bibliotecas de cliente têm funções assíncronas ou operações de longa duração. As bibliotecas que não precisam deles não criam linhas de execução em segundo plano.

Você pode notar outras linhas de execução no seu aplicativo, mas elas podem ser criadas por dependências da biblioteca de cliente C++, como o gRPC. Geralmente, essas linhas de execução são menos interessantes, porque nenhum código de aplicativo é executado nelas, e elas servem apenas para funções auxiliares.

Como esses encadeamentos em segundo plano afetam meu aplicativo?

Como de costume, essas linhas de execução competem por recursos de CPU e memória com o restante do aplicativo. Se necessário, você pode criar seu próprio pool de linhas de execução para ter um controle exato de todos os recursos que essas linhas usam. Confira os detalhes abaixo.

Algum código é executado em alguma dessas linhas de execução?

Sim. Quando você anexa um callback a um future<T>, ele quase sempre é executado por uma das linhas de execução em segundo plano. Isso só vai acontecer se o future<T> já for atendido no momento em que você anexa o callback. Nesse caso, o callback é executado imediatamente no contexto da linha de execução que o anexa.

Por exemplo, considere um aplicativo que usa a biblioteca de cliente do Pub/Sub. A chamada Publish() retorna um futuro e o aplicativo pode anexar um callback depois de executar algum trabalho:

namespace pubsub = ::google::cloud::pubsub;
namespace g = google::cloud;

void Callback(g::future<g::StatusOr<std::string>>);

void F(pubsub::Publisher publisher) {
  auto my_future = publisher.Publish(
      pubsub::MessageBuilder("Hello World!").Build());
  // do some work.
  my_future.then(Callback);
}

Se my_future for atendido antes da função .then() ser chamada, o callback será invocado imediatamente. Se você quiser garantir que o código seja executado em uma linha de execução separada, precisará usar seu próprio pool de linhas de execução e fornecer um elemento chamável em .then() que encaminhe a execução para esse pool.

Pools de linhas de execução padrão

Para as bibliotecas que exigem linhas de execução em segundo plano, o Make*Connection() cria um pool de linhas de execução padrão. A menos que você substitua o pool de linhas de execução, cada objeto *Connection tem um pool separado.

O pool de linhas de execução padrão na maioria das bibliotecas contém uma única linha de execução. Raramente, mais linhas de execução são necessárias, já que a linha em segundo plano é usada para pesquisar o estado de operações de longa duração. Essas chamadas são razoavelmente curtas e consomem pouca CPU. Portanto, uma única linha de execução em segundo plano pode lidar com centenas de operações pendentes de longa duração e muito poucos aplicativos.

Outras operações assíncronas podem exigir mais recursos. Use GrpcBackgroundThreadPoolSizeOption para mudar o tamanho padrão do pool de linhas de execução em segundo plano, se necessário.

A biblioteca do Pub/Sub espera ter muito mais trabalho, já que é comum que aplicativos Pub/Sub recebam ou enviem milhares de mensagens por segundo. Consequentemente, essa biblioteca tem como padrão uma linha de execução por núcleo em arquiteturas de 64 bits. Em arquiteturas de 32 bits (ou quando compiladas em modo de 32 bits, mesmo que sejam executadas em uma arquitetura de 64 bits), esse padrão muda para apenas quatro linhas de execução.

Como fornecer seu próprio pool de linhas de execução

Você pode fornecer seu próprio pool de linhas de execução para linhas de execução em segundo plano. Crie um objeto CompletionQueue, anexe linhas de execução a ele e configure o GrpcCompletionQueueOption ao inicializar o cliente. Exemplo:

namespace admin = ::google::cloud::spanner_admin;
namespace g = ::google::cloud;

void F() {
  // You will need to create threads
  auto cq = g::CompletionQueue();
  std::vector<std::jthread> threads;
  for (int i = 0; i != 10; ++i) {
    threads.emplace_back([](auto cq) { cq.Run(); }, cq);
  }
  auto client = admin::InstanceAdminClient(admin::MakeInstanceAdminConnection(
      g::Options{}.set<g::GrpcCompletionQueueOption>(cq)));
  // Use `client` as usual
}

É possível compartilhar o mesmo objeto CompletionQueue entre vários clientes, mesmo em serviços diferentes:

namespace admin = ::google::cloud::spanner_admin;
namespace admin = ::google::cloud::pubsub;
namespace g = ::google::cloud;

void F(pubsub::Topic const& topic1, pubsub::Topic const& topic2) {
  // You will need to create threads
  auto cq = g::CompletionQueue();
  std::vector<std::jthread> threads;
  for (int i = 0; i != 10; ++i) {
    threads.emplace_back([](auto cq) { cq.Run(); }, cq);
  }
  auto client = admin::InstanceAdminClient(admin::MakeInstanceAdminConnection(
      g::Options{}.set<g::GrpcCompletionQueue>(cq)));
  auto p1 = pubsub::Publisher(pubsub::MakePublisherConnection(
      topic1, g::Options{}.set<g::GrpcCompletionQueueOption>(cq)));
  auto p2 = pubsub::Publisher(pubsub::MakePublisherConnection(
      topic2, g::Options{}.set<g::GrpcCompletionQueueOption>(cq)));
  // Use `client`, `p1`, and `p2` as usual
}

Próximas etapas