Threads d'arrière-plan dans les bibliothèques clientes C++

Ce guide décrit le modèle de threads utilisé par les bibliothèques clientes C++ et vous montre comment remplacer le ou les pools de threads par défaut dans votre application.

Objectifs

  • Décrire le modèle de thread par défaut pour les bibliothèques clientes C++
  • Expliquez comment remplacer ces valeurs par défaut pour les applications qui le nécessitent.

Pourquoi les bibliothèques clientes utilisent-elles des threads en arrière-plan ?

La plupart des fonctions des bibliothèques clientes utilisent le thread qui appelle la fonction pour effectuer toutes les tâches, y compris les RPC au service et/ou l'actualisation des jetons d'accès pour l'authentification.

Les fonctions asynchrones, par leur nature, ne peuvent pas utiliser le thread actuel pour effectuer leur travail. Un thread distinct doit attendre la fin de la tâche et gérer la réponse.

Il est également inutile de bloquer le thread appelant pour les opérations de longue durée, car le service peut mettre plusieurs minutes à terminer le travail. Pour ces opérations, la bibliothèque cliente utilise des threads en arrière-plan pour interroger régulièrement l'état de l'opération de longue durée.

Quelles fonctions et bibliothèques nécessitent des threads en arrière-plan ?

Les fonctions qui renvoient un future<T> pour un certain type T utilisent des threads en arrière-plan pour attendre la fin de la tâche.

Les bibliothèques clientes ne disposent pas toutes de fonctions asynchrones ou d'opérations de longue durée. Les bibliothèques qui n'en ont pas besoin ne créent aucun thread en arrière-plan.

Vous remarquerez peut-être des threads supplémentaires dans votre application, mais ceux-ci peuvent être créés par des dépendances de la bibliothèque cliente C++, telles que gRPC. Ces threads sont généralement moins intéressants, car aucun code d'application ne s'exécute jamais dans ces threads et ils ne servent que des fonctions auxiliaires.

Comment ces threads en arrière-plan affectent-ils mon application ?

Comme à l'accoutumée, ces threads sont en concurrence pour les ressources de processeur et de mémoire avec le reste de votre application. Si nécessaire, vous pouvez créer votre propre pool de threads pour contrôler précisément les ressources que ces threads utilisent. Pour en savoir plus, consultez les informations ci-dessous.

Une partie de mon code s'exécute-t-elle dans l'un de ces threads ?

Oui. Lorsque vous associez un rappel à un future<T>, le rappel est presque toujours exécuté par l'un des threads en arrière-plan. Le seul cas où cela ne se produit pas est si future<T> est déjà satisfait au moment où vous associez le rappel. Dans ce cas, le rappel s'exécute immédiatement, dans le contexte du thread qui associe le rappel.

Prenons l'exemple d'une application qui utilise la bibliothèque cliente Pub/Sub. L'appel Publish() renvoie un objet Future, et l'application peut joindre un rappel après avoir effectué une tâche:

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);
}

Si my_future est satisfait avant l'appel de la fonction .then(), le rappel est invoqué immédiatement. Pour vous assurer que le code s'exécute dans un thread distinct, vous devez utiliser votre propre pool de threads et fournir un appelable dans .then() qui transfère l'exécution à votre pool de threads.

Pools de threads par défaut

Pour les bibliothèques qui nécessitent des threads en arrière-plan, Make*Connection() crée un pool de threads par défaut. Chaque objet *Connection possède un pool de threads distinct, sauf si vous remplacez le pool de threads.

Dans la plupart des bibliothèques, le pool de threads par défaut ne contient qu'un seul thread. Il est rarement nécessaire d'avoir plus de threads, car le thread d'arrière-plan est utilisé pour interroger l'état des opérations de longue durée. Ces appels ont une durée de vie raisonnablement courte et consomment très peu de processeur. Par conséquent, un seul thread en arrière-plan peut gérer des centaines d'opérations de longue durée, et très peu d'applications en ont même autant.

D'autres opérations asynchrones peuvent nécessiter davantage de ressources. Si nécessaire, utilisez GrpcBackgroundThreadPoolSizeOption pour modifier la taille par défaut du pool de threads d'arrière-plan.

La bibliothèque Pub/Sub s'attend à ce que votre travail soit beaucoup plus complexe, car il est courant que les applications Pub/Sub reçoivent ou envoient des milliers de messages par seconde. Par conséquent, cette bibliothèque utilise par défaut un thread par cœur sur les architectures 64 bits. Sur les architectures 32 bits (ou en cas de compilation en mode 32 bits, même si vous utilisez une architecture 64 bits), cette valeur par défaut ne passe qu'à quatre threads.

Fournir votre propre pool Thread

Vous pouvez fournir votre propre pool de threads pour les threads en arrière-plan. Créez un objet CompletionQueue, associez-lui des threads et configurez GrpcCompletionQueueOption lors de l'initialisation du client. Exemple :

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
}

Vous pouvez partager le même objet CompletionQueue entre plusieurs clients, même pour différents services:

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
}

Étapes suivantes