Subprocesos en segundo plano en las bibliotecas cliente de C++

En esta guía, se describe el modelo de subprocesos que usan las bibliotecas cliente de C++ y se muestra cómo anular los grupos de subprocesos predeterminados en tu aplicación.

Objetivos

  • Describe el modelo de subprocesos predeterminado para las bibliotecas cliente de C++.
  • Describe cómo anular estos valores predeterminados para las aplicaciones que lo necesiten.

¿Por qué las bibliotecas cliente usan subprocesos en segundo plano?

La mayoría de las funciones de las bibliotecas cliente usan el subproceso que llama a la función para completar todo el trabajo, incluidas las RPC al servicio o la actualización de los tokens de acceso para la autenticación.

Por naturaleza, las funciones asíncronas no pueden usar el subproceso actual para completar su trabajo. Un subproceso independiente debe esperar a que se complete el trabajo y manejar la respuesta.

Además, es una pérdida de recursos bloquear el subproceso de llamada en operaciones de larga duración, en las que el servicio puede tardar minutos o más en completar la tarea. Para estas operaciones, la biblioteca cliente usa subprocesos en segundo plano a fin de sondear periódicamente el estado de la operación de larga duración.

¿Qué funciones y bibliotecas requieren subprocesos en segundo plano?

Las funciones que muestran un future<T> para algún tipo de T usan subprocesos en segundo plano a fin de esperar hasta que se complete el trabajo.

No todas las bibliotecas cliente tienen operaciones asíncronas o de larga duración. Las bibliotecas que no los necesitan no crean subprocesos en segundo plano.

Es posible que observes subprocesos adicionales en tu aplicación, pero estos pueden ser creados por dependencias de la biblioteca cliente de C++, como gRPC. Estos subprocesos suelen ser menos interesantes, ya que ningún código de la aplicación se ejecuta en estos subprocesos y solo cumplen funciones auxiliares.

¿Cómo afectan a mi aplicación estos subprocesos en segundo plano?

Como siempre, estos subprocesos compiten por recursos de CPU y memoria con el resto de tu aplicación. Si es necesario, puedes crear tu propio conjunto de subprocesos para tener un control detallado de los recursos que usan estos subprocesos. A continuación encontrarás más información.

¿Se ejecuta parte de mi código en alguno de estos subprocesos?

Sí. Cuando adjuntas una devolución de llamada a un future<T>, uno de los subprocesos en segundo plano casi siempre la ejecuta. El único caso en el que esto no sucedería es si future<T> ya estaba satisfecha para el momento en que adjuntaste la devolución de llamada. En ese caso, la devolución de llamada se ejecuta de inmediato, en el contexto del subproceso que adjunta la devolución de llamada.

Por ejemplo, considera una aplicación que usa la biblioteca cliente de Pub/Sub. La llamada a Publish() muestra un futuro y la aplicación puede adjuntar una devolución de llamada después de realizar algunas tareas:

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 se cumple my_future antes de que se llame a la función .then(), la devolución de llamada se invoca de inmediato. Si deseas garantizar que el código se ejecute en un subproceso independiente, debes usar tu propio conjunto de subprocesos y proporcionar una función que admita llamadas en .then(), que reenvíe la ejecución al conjunto de subprocesos.

Grupos de subprocesos predeterminados

Para aquellas bibliotecas que requieren subprocesos en segundo plano, Make*Connection() crea un conjunto de subprocesos predeterminado. A menos que anules el conjunto de subprocesos, cada objeto *Connection tiene un conjunto de subprocesos independiente.

En la mayoría de las bibliotecas, el conjunto predeterminado de subprocesos contiene un solo subproceso. Rara vez se necesitan más subprocesos, ya que el subproceso en segundo plano se usa para sondear el estado de las operaciones de larga duración. Estas llamadas tienen una duración razonablemente corta y consumen muy poca CPU, por lo que un solo subproceso en segundo plano puede controlar cientos de operaciones pendientes de larga duración, y muy pocas aplicaciones tienen esa cantidad.

Otras operaciones asíncronas pueden requerir más recursos. Usa GrpcBackgroundThreadPoolSizeOption para cambiar el tamaño predeterminado del conjunto de subprocesos en segundo plano si es necesario.

La biblioteca de Pub/Sub espera tener mucho más trabajo, ya que es común que las aplicaciones de Pub/Sub reciban o envíen miles de mensajes por segundo. En consecuencia, la biblioteca se establece de forma predeterminada en un subproceso por núcleo en arquitecturas de 64 bits. En arquitecturas de 32 bits (o cuando se compila en modo de 32 bits, incluso si se ejecuta en una arquitectura de 64 bits), esta configuración predeterminada cambia a solo 4 subprocesos.

Cómo crear tu propio Thread Pool

Puedes proporcionar tu propio conjunto de subprocesos para los subprocesos en segundo plano. Crea un objeto CompletionQueue, adjunta subprocesos y configura GrpcCompletionQueueOption cuando inicialices tu cliente. Por ejemplo:

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
}

Puedes compartir el mismo objeto CompletionQueue entre varios clientes, incluso para servicios diferentes:

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
}

Próximos pasos