Hintergrundthreads in C++-Clientbibliotheken

In diesem Leitfaden wird das Threading-Modell beschrieben, das von den C++-Clientbibliotheken verwendet wird. Außerdem erfahren Sie, wie Sie die Standard-Thread-Pools in Ihrer Anwendung überschreiben.

Lernziele

  • Beschreiben Sie das Standard-Threading-Modell für die C++-Clientbibliotheken.
  • Beschreiben Sie, wie diese Standardeinstellungen für Anwendungen überschrieben werden können, die dies erfordern.

Warum verwenden die Clientbibliotheken Hintergrundthreads?

Die meisten Funktionen in den Clientbibliotheken verwenden den Thread, der die Funktion aufruft, um die gesamte Arbeit auszuführen, einschließlich aller RPCs an den Dienst und/oder Aktualisieren von Zugriffstokens für die Authentifizierung.

Asynchrone Funktionen können von Natur aus nicht den aktuellen Thread verwenden, um ihre Arbeit abzuschließen. Ein separater Thread muss warten, bis die Arbeit abgeschlossen ist und die Antwort verarbeitet wird.

Es ist auch aufwendig, den aufrufenden Thread bei Vorgängen mit langer Ausführungszeit zu blockieren, da es dann einige Minuten oder länger dauern kann, bis der Dienst die Arbeit abgeschlossen hat. Für solche Vorgänge verwendet die Clientbibliothek Hintergrundthreads, um den Status von lang andauernden Vorgängen regelmäßig abzufragen.

Welche Funktionen und Bibliotheken erfordern Hintergrundthreads?

Funktionen, die ein future<T> für einen T-Typ zurückgeben, verwenden Hintergrundthreads, um zu warten, bis die Arbeit abgeschlossen ist.

Nicht alle Clientbibliotheken haben asynchrone Funktionen oder Vorgänge mit langer Ausführungszeit. Bibliotheken, die sie nicht benötigen, erstellen keine Hintergrundthreads.

Möglicherweise bemerken Sie zusätzliche Threads in Ihrer Anwendung, die jedoch durch Abhängigkeiten der C++-Clientbibliothek wie gRPC erstellt werden können. Diese Threads sind in der Regel weniger interessant, da in diesen Threads kein Anwendungscode ausgeführt wird und sie nur ergänzende Funktionen erfüllen.

Wie wirken sich diese Hintergrundthreads auf meine Anwendung aus?

Wie üblich konkurrieren diese Threads mit dem Rest Ihrer Anwendung um CPU- und Arbeitsspeicherressourcen. Bei Bedarf können Sie einen eigenen Threadpool erstellen, um die von diesen Threads verwendeten Ressourcen genau zu steuern. Weitere Informationen dazu finden Sie unten.

Wird in einem dieser Threads von meinem Code ausgeführt?

Ja. Wenn Sie einen Callback an einen future<T> anhängen, wird dieser fast immer von einem der Hintergrundthreads ausgeführt. Dies wäre nur dann nicht möglich, wenn future<T> bereits erfüllt ist, wenn der Callback angehängt wird. In diesem Fall wird der Callback sofort im Kontext des Threads ausgeführt, an den der Callback angehängt wird.

Angenommen, Sie haben eine Anwendung, die die Pub/Sub-Clientbibliothek verwendet. Der Aufruf Publish() gibt ein Future zurück und die Anwendung kann nach Ausführung einiger Schritte einen Callback anhängen:

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

Wenn my_future erfüllt ist, bevor die Funktion .then() aufgerufen wird, wird der Callback sofort aufgerufen. Wenn Sie sicherstellen möchten, dass der Code in einem separaten Thread ausgeführt wird, müssen Sie Ihren eigenen Thread-Pool verwenden und ein Callable in .then() bereitstellen, das die Ausführung an Ihren Thread-Pool weiterleitet.

Standard-Thread-Pools

Für Bibliotheken, die Hintergrundthreads benötigen, erstellt Make*Connection() einen Standardthreadpool. Wenn Sie den Thread-Pool nicht überschreiben, hat jedes *Connection-Objekt einen separaten Thread-Pool.

Der Standardthreadpool in den meisten Bibliotheken enthält einen einzelnen Thread. Selten werden mehr Threads benötigt, da der Hintergrundthread dazu verwendet wird, den Status lang andauernder Vorgänge abzufragen. Diese Aufrufe sind relativ kurzlebig und verbrauchen sehr wenig CPU, sodass ein einzelner Hintergrundthread Hunderte von ausstehenden lang andauernden Vorgängen verarbeiten kann. Nur sehr wenige Anwendungen haben sogar so viele.

Für andere asynchrone Vorgänge sind möglicherweise mehr Ressourcen erforderlich. Verwenden Sie GrpcBackgroundThreadPoolSizeOption, um bei Bedarf die Standardgröße des Hintergrundthreadpools zu ändern.

Die Pub/Sub-Bibliothek erwartet wesentlich mehr Arbeit, da Pub/Sub-Anwendungen oft Tausende von Nachrichten pro Sekunde empfangen oder senden. Daher verwendet diese Bibliothek auf 64-Bit-Architekturen standardmäßig einen Thread pro Kern. Bei 32-Bit-Architekturen (oder bei Kompilierung im 32-Bit-Modus, selbst wenn sie auf einer 64-Bit-Architektur ausgeführt werden) ändert sich diese Standardeinstellung in nur 4 Threads.

Eigenen Thread-Pool bereitstellen

Sie können einen eigenen Thread-Pool für Hintergrundthreads bereitstellen. Erstellen Sie ein CompletionQueue-Objekt, hängen Sie Threads an dieses Objekt an und konfigurieren Sie GrpcCompletionQueueOption beim Initialisieren des Clients. Beispiel:

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
}

Sie können dasselbe CompletionQueue-Objekt für mehrere Clients verwenden, auch für verschiedene Dienste:

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
}

Nächste Schritte