Thread in background nelle librerie client di C++

Questa guida descrive il modello di thread utilizzato dalle librerie client di C++ e mostra come eseguire l'override dei pool di thread predefiniti nella tua applicazione.

Obiettivi

  • Descrivi il modello di threading predefinito per le librerie client di C++.
  • Descrivi come eseguire l'override di queste impostazioni predefinite per le applicazioni che devono farlo.

Perché le librerie client utilizzano i thread in background?

La maggior parte delle funzioni nelle librerie client utilizza il thread che chiama la funzione per completare tutto il lavoro, comprese eventuali RPC al servizio e/o l'aggiornamento dei token di accesso per l'autenticazione.

Per loro natura, le funzioni asincrone non possono utilizzare il thread corrente per completare il lavoro. Alcuni thread separati devono attendere il completamento del lavoro e gestire la risposta.

Inoltre, bloccare il thread di chiamata nelle operazioni a lunga esecuzione è uno spreco di risorse, in cui potrebbero essere necessari minuti o più prima che il servizio completi l'operazione. Per queste operazioni, la libreria client utilizza i thread in background per eseguire periodicamente il polling dello stato dell'operazione a lunga esecuzione.

Quali funzioni e librerie richiedono i thread in background?

Le funzioni che restituiscono un future<T> per un tipo T utilizzano i thread in background per attendere il completamento del lavoro.

Non tutte le librerie client hanno funzioni asincrone o operazioni a lunga esecuzione. Le librerie che non ne hanno bisogno non creano thread in background.

Potresti notare thread aggiuntivi nell'applicazione, ma potrebbero essere creati dalle dipendenze della libreria client C++, come gRPC. Questi thread sono generalmente meno interessanti perché in questi thread non viene mai eseguito alcun codice dell'applicazione e svolgono solo funzioni accessorie.

In che modo questi thread in background influiscono sulla mia applicazione?

Come di consueto, questi thread competono per le risorse di CPU e memoria con il resto dell'applicazione. Se necessario, puoi creare il tuo pool di thread per ottenere il controllo preciso delle risorse utilizzate da questi thread. Vedi i dettagli di seguito.

Il mio codice viene eseguito in uno di questi thread?

Sì. Quando alleghi un callback a una future<T>, il callback viene quasi sempre eseguito da uno dei thread in background. L'unico caso in cui ciò non si verifica è se future<T> è già soddisfatto quando alleghi la chiamata. In questo caso il callback viene eseguito immediatamente, nel contesto del thread che collega il callback.

Prendi in considerazione, ad esempio, un'applicazione che utilizza la libreria client Pub/Sub. La chiamata Publish() restituisce un futuro e l'applicazione può collegare un callback dopo aver eseguito alcune operazioni:

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 viene soddisfatto prima della chiamata della funzione .then(), il callback viene richiamato immediatamente. Se vuoi garantire che il codice venga eseguito in un thread separato, devi utilizzare il tuo pool di thread e fornire un elemento richiamabile in .then() che inoltra l'esecuzione al tuo pool di thread.

Pool di thread predefiniti

Per le librerie che richiedono thread in background, Make*Connection() crea un pool di thread predefinito. A meno che non esegui l'override del pool di thread, ogni oggetto *Connection ha un pool di thread separato.

Il pool di thread predefinito nella maggior parte delle librerie contiene un singolo thread. Raramente sono necessari più thread poiché quello in background viene utilizzato per eseguire il polling dello stato delle operazioni a lunga esecuzione. Queste chiamate hanno durata ragionevolmente breve e consumano poca CPU, pertanto un singolo thread in background può gestire centinaia di operazioni a lunga esecuzione in attesa e pochissime applicazioni ne hanno molte.

Altre operazioni asincrone potrebbero richiedere più risorse. Utilizza GrpcBackgroundThreadPoolSizeOption per modificare le dimensioni predefinite del pool di thread in background, se necessario.

La libreria Pub/Sub prevede di impiegare molto più lavoro, poiché è comune che le applicazioni Pub/Sub ricevano o inviino migliaia di messaggi al secondo. Di conseguenza, il valore predefinito di questa libreria è un thread per core nelle architetture a 64 bit. Nelle architetture a 32 bit (o quando compilate in modalità a 32 bit, anche se in esecuzione su un'architettura a 64 bit), questo valore predefinito cambia in solo 4 thread.

Fornire il proprio pool di Thread

Puoi fornire il tuo pool di thread per i thread in background. Crea un oggetto CompletionQueue, collegavi i thread e configura GrpcCompletionQueueOption durante l'inizializzazione del client. Ad esempio:

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
}

Puoi condividere lo stesso oggetto CompletionQueue tra più client, anche per servizi diversi:

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
}

Passaggi successivi