Ce guide décrit le modèle de threading 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écrivez le modèle de threading par défaut pour les bibliothèques clientes C++.
- Décrivez comment remplacer ces valeurs par défaut pour les applications qui en ont besoin.
Pourquoi les bibliothèques clientes utilisent-elles des threads d'arrière-plan ?
La plupart des fonctions des bibliothèques clientes utilisent le thread appelant 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 nature, ne peuvent pas utiliser le thread actuel pour terminer leur travail. Un thread distinct doit attendre la fin du travail et gérer la réponse.
Il est également inutile de bloquer le thread appelant sur des opérations de longue durée, où le service peut prendre plusieurs minutes ou plus pour 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 d'arrière-plan ?
Les fonctions qui renvoient un future<T> pour un type T utilisent des threads en arrière-plan pour attendre la fin du travail.
Toutes les bibliothèques clientes ne disposent pas de fonctions asynchrones ni d'opérations de longue durée. Les bibliothèques qui n'en ont pas besoin ne créent aucun thread d'arrière-plan.
Vous remarquerez peut-être des threads supplémentaires dans votre application, mais ils 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'y exécute et ils ne servent que des fonctions auxiliaires.
Quel est l'impact de ces threads d'arrière-plan sur mon application ?
Comme d'habitude, ces threads sont en concurrence avec le reste de votre application pour les ressources de processeur et de mémoire. Si nécessaire, vous pouvez créer votre propre pool de threads pour contrôler précisément les ressources utilisées par ces threads. Pour en savoir plus, consultez les informations ci-dessous.
Mon code s'exécute-t-il 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 d'arrière-plan. La seule exception à cette règle est le cas où 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 auquel il est associé.
Prenons l'exemple d'une application utilisant la bibliothèque cliente Pub/Sub.
L'appel Publish() renvoie un futur et l'application peut associer un rappel après avoir effectué certaines tâches :
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. Si vous souhaitez garantir 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 d'arrière-plan, Make*Connection() crée un pool de threads par défaut. Sauf si vous remplacez le pool de threads, chaque objet *Connection possède un pool de threads distinct.
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'utiliser plus de threads, car le thread d'arrière-plan sert à interroger l'état des opérations de longue durée. Ces appels sont relativement brefs et consomment très peu de ressources processeur. Un seul thread en arrière-plan peut donc gérer des centaines d'opérations de longue durée en attente, et très peu d'applications en ont autant.
D'autres opérations asynchrones peuvent nécessiter davantage de ressources.
Utilisez GrpcBackgroundThreadPoolSizeOption pour modifier la taille du pool de threads d'arrière-plan par défaut si nécessaire.
La bibliothèque Pub/Sub s'attend à avoir beaucoup plus de travail, 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 est définie par défaut sur un thread par cœur sur les architectures 64 bits. Sur les architectures 32 bits (ou lorsqu'il est compilé en mode 32 bits, même s'il est exécuté sur une architecture 64 bits), cette valeur par défaut passe à seulement quatre threads.
Fournir votre propre pool de threads
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 de votre 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 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
}
Étapes suivantes
- Consultez la section Configuration de la bibliothèque cliente pour en savoir plus sur les options de configuration courantes des bibliothèques.
- Consultez la bibliothèque cliente C++ Cloud Pub/Sub pour obtenir la documentation de référence complète sur la bibliothèque cliente Pub/Sub.