Thread in background nelle librerie client C++

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

Obiettivi

  • Descrivi il modello di threading predefinito per le librerie client C++.
  • Descrivi come ignorare questi valori predefiniti per le applicazioni che ne hanno bisogno.

Perché le librerie client utilizzano thread in background?

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

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

È anche uno spreco bloccare il thread di chiamata per le operazioni a lunga esecuzione, in cui il servizio potrebbe impiegare minuti o più per completare il lavoro. Per queste operazioni, la libreria client utilizza thread in background per eseguire periodicamente il polling dello stato delloperazione a lunga esecuzione.

Quali funzioni e librerie richiedono thread in background?

Le funzioni che restituiscono un future<T> per un determinato tipo T utilizzano thread in background per attendere il completamento dell'operazione.

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 nella tua applicazione, ma questi potrebbero essere creati dalle dipendenze della libreria client C++, come gRPC. Questi thread sono in genere meno interessanti, perché in questi thread non viene mai eseguito codice dell'applicazione e svolgono solo funzioni ausiliarie.

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 un controllo preciso delle risorse utilizzate da questi thread. Vedi i dettagli di seguito.

Qualche parte del mio codice viene eseguita in uno di questi thread?

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

Ad esempio, considera un'applicazione che utilizza la libreria client Pub/Sub. La chiamata Publish() restituisce un futuro e l'applicazione può allegare 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 soddisfatta 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 callable in .then() che inoltri 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 tu non esegua 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 solo thread. Sono raramente necessari più thread, poiché il thread in background viene utilizzato per eseguire il polling dello stato delle operazioni a lunga esecuzione. Queste chiamate hanno una durata ragionevolmente breve e consumano pochissima CPU, quindi un singolo thread in background può gestire centinaia di operazioni a lunga esecuzione in attesa e pochissime applicazioni ne hanno così tante.

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

La libreria Pub/Sub prevede un carico di lavoro molto maggiore, poiché è comune che le applicazioni Pub/Sub ricevano o inviino migliaia di messaggi al secondo. Di conseguenza, questa libreria utilizza per impostazione predefinita un thread per core su architetture a 64 bit. Nelle architetture a 32 bit (o quando viene compilato in modalità a 32 bit, anche se viene eseguito su un'architettura a 64 bit), questo valore predefinito cambia in soli 4 thread.

Fornire il proprio pool di thread

Puoi fornire il tuo pool di thread per i thread in background. Crea un oggetto CompletionQueue, allega 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ù clienti, anche per servizi diversi:

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
}

Passaggi successivi