Hintergrundthreads in den C++-Clientbibliotheken

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

Ziele

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

Warum verwenden die Clientbibliotheken Hintergrundthreads?

Die meisten Funktionen in den Clientbibliotheken verwenden den Thread, der die Funktion aufruft, um alle Aufgaben zu erledigen, einschließlich aller RPCs an den Dienst und/oder das Aktualisieren von Zugriffstokens für die Authentifizierung.

Asynchrone Funktionen können naturgemäß nicht den aktuellen Thread verwenden, um ihre Arbeit zu erledigen. Ein separater Thread muss warten, bis die Arbeit abgeschlossen ist, und die Antwort verarbeiten.

Außerdem ist es ineffizient, den aufrufenden Thread bei Vorgängen mit langer Ausführungszeit zu blockieren, da es 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 des lang andauernden Vorgangs regelmäßig abzufragen.

Welche Funktionen und Bibliotheken erfordern Hintergrundthreads?

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

Nicht alle Clientbibliotheken haben asynchrone Funktionen oder Vorgänge, die lange dauern. Bibliotheken, die keine Hintergrundthreads benötigen, erstellen auch keine.

Möglicherweise werden in Ihrer Anwendung zusätzliche Threads erstellt. Diese können jedoch von Abhängigkeiten der C++-Clientbibliothek wie gRPC erstellt werden. Diese Threads sind in der Regel weniger interessant, da in ihnen kein Anwendungscode ausgeführt wird und sie nur Hilfsfunktionen erfüllen.

Wie wirken sich diese Hintergrund-Threads auf meine Anwendung aus?

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

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

Ja. Wenn Sie einen Callback an ein future<T> anhängen, wird der Callback fast immer von einem der Hintergrundthreads ausgeführt. Das ist nur dann nicht der Fall, wenn die future<T> bereits erfüllt ist, wenn Sie den Callback anhängen. In diesem Fall wird der Callback sofort im Kontext des Threads ausgeführt, der den Callback anhängt.

Betrachten Sie beispielsweise eine Anwendung, die die Pub/Sub-Clientbibliothek verwendet. Der Publish()-Aufruf gibt ein Future zurück und die Anwendung kann nach der Ausführung einiger Aufgaben 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 garantieren möchten, dass der Code in einem separaten Thread ausgeführt wird, müssen Sie einen eigenen Thread-Pool verwenden und ein Callable in .then() bereitstellen, das die Ausführung an Ihren Thread-Pool weiterleitet.

Standard-Threadpools

Für Bibliotheken, die Hintergrundthreads erfordern, wird mit Make*Connection() ein Standard-Threadpool erstellt. Sofern Sie den Threadpool nicht überschreiben, hat jedes *Connection-Objekt einen separaten Threadpool.

Der Standard-Threadpool in den meisten Bibliotheken enthält einen einzelnen Thread. Weitere Threads sind selten erforderlich, da der Hintergrundthread zum Abrufen des Status von Vorgängen mit langer Ausführungszeit verwendet wird. Diese Aufrufe sind relativ kurzlebig und verbrauchen sehr wenig CPU. Ein einzelner Hintergrundthread kann daher Hunderte von ausstehenden, lang andauernden Vorgängen verarbeiten. Nur sehr wenige Anwendungen haben so viele.

Andere asynchrone Vorgänge erfordern möglicherweise mehr Ressourcen. Verwenden Sie GrpcBackgroundThreadPoolSizeOption, um die Standardgröße des Hintergrund-Thread-Pools bei Bedarf zu ändern.

Die Pub/Sub-Bibliothek erwartet deutlich mehr Arbeit, da Pub/Sub-Anwendungen in der Regel Tausende von Nachrichten pro Sekunde empfangen oder senden. Daher wird in dieser Bibliothek standardmäßig ein Thread pro Kern auf 64-Bit-Architekturen verwendet. Bei 32-Bit-Architekturen (oder wenn in 32-Bit-Modus kompiliert wird, auch wenn die Ausführung auf einer 64-Bit-Architektur erfolgt) ändert sich dieser Standardwert auf nur 4 Threads.

Eigenen Threadpool bereitstellen

Sie können einen eigenen Thread-Pool für Hintergrundthreads bereitstellen. Erstellen Sie ein CompletionQueue-Objekt, hängen Sie Threads daran 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 freigeben, auch für verschiedene Dienste:

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
}

Nächste Schritte