Este guia descreve o modelo de linhas de execução usado pelas bibliotecas de cliente C++ e mostra como substituir os pools de linhas de execução padrão no seu aplicativo.
Objetivos
- Descrever o modelo de threading padrão para as bibliotecas de cliente C++.
- Descreva como substituir esses padrões para aplicativos que precisam disso.
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 RPCs para o serviço e/ou atualização de tokens de acesso para autenticação.
As funções assíncronas, por natureza, não podem usar a linha de execução atual para concluir o trabalho. Alguma thread separada precisa esperar a conclusão do trabalho e processar a resposta.
Também é um desperdício 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 sondar 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 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 delas não criam threads em segundo plano.
Você pode notar outras linhas de execução no aplicativo, mas elas podem ser criadas por dependências da biblioteca de cliente C++, como o gRPC. Essas linhas de execução geralmente são menos interessantes, porque nenhum código de aplicativo é executado nelas, e elas servem apenas a funções auxiliares.
Como essas linhas de execução 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 seu aplicativo. Se necessário, crie seu próprio pool de linhas de execução para ter controle refinado de todos os recursos usados por elas. Veja os detalhes abaixo.
Algum dos meus códigos é 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ó não vai acontecer se a future<T> já estiver satisfeita quando você anexar o callback. Nesse caso, o callback é executado imediatamente no contexto da
thread 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 realizar 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 atendida antes da chamada da função .then(), o
callback será invocado imediatamente. Se quiser garantir que o código seja executado em uma
thread separada, use seu próprio pool de threads e forneça um objeto invocável em
.then() que encaminhe a execução para seu pool de threads.
Pools de threads padrão
Para as bibliotecas que exigem threads em segundo plano, o Make*Connection()
cria um pool de threads 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. Mais linhas de execução raramente são necessárias, já que a linha de execução em segundo plano é usada para pesquisar o estado de operações de longa duração. Essas chamadas são razoavelmente de curta duração e consomem muito pouca CPU. Assim, uma única thread em segundo plano pode processar centenas de operações pendentes de longa duração, e muito poucos aplicativos têm até mesmo tantas.
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 os aplicativos do Pub/Sub recebam ou enviem milhares de mensagens por segundo. Consequentemente, essa biblioteca usa uma linha de execução por núcleo em arquiteturas de 64 bits. Em arquiteturas de 32 bits (ou quando compilado no modo de 32 bits, mesmo se estiver sendo executado em uma arquitetura de 64 bits), esse padrão muda para apenas quatro linhas de execução.
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 em vários clientes, mesmo
para serviços diferentes:
namespace admin = ::google::cloud::spanner_admin;
namespace pubsub = ::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
- Consulte Configuração da biblioteca de cliente para saber mais sobre as opções comuns de configuração da biblioteca.
- Consulte a biblioteca de cliente C++ do Cloud Pub/Sub para acessar a documentação de referência completa da biblioteca de cliente do Pub/Sub.