C++ 클라이언트 라이브러리의 백그라운드 스레드

이 가이드에서는 C++ 클라이언트 라이브러리에서 사용하는 스레딩 모델을 설명하고 애플리케이션의 기본 스레드 풀을 재정의하는 방법을 보여줍니다.

목표

  • C++ 클라이언트 라이브러리의 기본 스레딩 모델을 설명합니다.
  • 필요한 경우 애플리케이션에 이러한 기본값을 재정의하는 방법을 설명합니다.

클라이언트 라이브러리가 백그라운드 스레드를 사용하는 이유는 무엇인가요?

클라이언트 라이브러리의 대부분의 함수는 함수를 호출하는 스레드를 사용하여 모든 작업을 완료합니다. 여기에는 서비스에 대한 RPC 및 인증을 위한 액세스 토큰 새로고침이 포함됩니다.

비동기 함수는 기본적으로 현재 스레드를 사용하여 작업을 완료할 수 없습니다. 일부 개별 스레드는 작업이 완료되고 응답을 처리할 때까지 기다려야 합니다.

또한 서비스가 작업을 완료하는 데 몇 분 이상 걸릴 수 있는 장기 실행 작업에 대해 호출 스레드를 차단하는 것도 소용 없습니다. 이러한 작업의 경우 클라이언트 라이브러리는 백그라운드 스레드를 사용하여 장기 실행 작업의 상태를 주기적으로 폴링합니다.

어떤 함수와 라이브러리에 백그라운드 스레드가 필요한가요?

일부 T 유형에 대해 future<T>을 반환하는 함수는 백그라운드 스레드를 사용하여 작업이 완료될 때까지 기다립니다.

모든 클라이언트 라이브러리에 비동기 함수 또는 장기 실행 작업이 있는 것은 아닙니다. 이러한 사항이 필요하지 않은 라이브러리는 백그라운드 스레드를 만들지 않습니다.

애플리케이션에서 추가 스레드가 표시될 수 있지만 gRPC와 같은 C++ 클라이언트 라이브러리의 종속 항목으로 생성될 수 있습니다. 이러한 스레드는 애플리케이션 코드를 실행하지 않고 보조 함수만 제공하기 때문에 일반적으로 관심이 적습니다.

이러한 백그라운드 스레드는 애플리케이션에 어떤 영향을 주나요?

평소와 같이 이 스레드는 애플리케이션의 나머지와 CPU 및 메모리 리소스를 경합합니다. 필요한 경우 자체 스레드 풀을 만들어 이러한 스레드가 사용하는 모든 리소스를 세밀하게 제어할 수 있습니다. 세부정보는 다음을 참고하세요.

내 코드가 일부라도 이러한 스레드에서 실행되나요?

예. future<T>에 콜백을 연결하면 콜백은 거의 항상 백그라운드 스레드 중 하나에서 실행됩니다. 그렇지 않은 유일한 경우는 콜백을 연결할 때까지 future<T>이 충족된 경우입니다. 이 경우 콜백이 즉시 실행되며, 콜백을 연결하는 스레드 컨텍스트에서 실행됩니다.

예를 들어 Pub/Sub 클라이언트 라이브러리를 사용하는 애플리케이션을 생각해 보세요. Publish() 호출은 future를 반환하고 애플리케이션은 일부 작업 수행 후 콜백을 연결할 수 있습니다.

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

.then() 함수가 호출되기 전에 my_future이 충족되면 콜백이 즉시 호출됩니다. 코드가 별도의 스레드에서 실행되도록 하려면 자체 스레드 풀을 사용하고 호출 가능 항목을 실행을 스레드 풀에 전달하는 .then()에 제공해야 합니다.

기본 스레드 풀

백그라운드 스레드가 필요한 라이브러리의 경우 Make*Connection()을 통해 기본 스레드 풀이 만들어집니다. 스레드 풀을 재정의하지 않으면 각 *Connection 객체에 개별 스레드 풀이 포함됩니다.

대부분의 라이브러리의 기본 스레드 풀에는 단일 스레드가 포함됩니다. 백그라운드 스레드는 장기 실행 작업의 상태를 폴링하는 데 사용되므로 더 많은 스레드가 필요하지 않습니다. 이러한 호출은 비교적 수명이 짧으며 CPU를 매우 적게 소비하므로 단일 백그라운드 스레드가 대기 중인 장기 실행 작업 수백 개를 처리할 수 있으며 작업이 그렇게 많은 애플리케이션도 거의 없습니다.

다른 비동기 작업에 더 많은 리소스가 필요할 수 있습니다. 필요한 경우 GrpcBackgroundThreadPoolSizeOption을 사용하여 기본 백그라운드 스레드 풀 크기를 변경합니다.

Pub/Sub 애플리케이션이 초당 수천 개의 메시지를 받거나 보내는 것이 일반적이므로 Pub/Sub 라이브러리에는 훨씬 더 많은 작업이 필요할 수 있습니다. 따라서 이 라이브러리의 기본값은 64비트 아키텍처에서 코어당 스레드 1개입니다. 32비트 아키텍처(또는 64비트 아키텍처에서 실행되는 경우에도 32비트 모드에서 컴파일되는 경우)의 기본값은 스레드 4개로만 변경됩니다.

자체 스레드 풀 제공

백그라운드 스레드에 자체 스레드 풀을 제공할 수 있습니다. CompletionQueue 객체를 만들고, 스레드를 연결하고, 클라이언트를 초기화할 때 GrpcCompletionQueueOption을 구성합니다. 예를 들면 다음과 같습니다.

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
}

다른 서비스에 대해서도 동일한 CompletionQueue 객체를 여러 클라이언트에서 공유할 수 있습니다.

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
}

다음 단계