GRPC场景选型:RPC返回google.protobuf.Empty时选异步还是同步客户端API
C++ gRPC客户端无返回值场景的API选择最佳实践
核心场景回顾
客户端响应用户事件发起RPC调用,所有调用返回google.protobuf.Empty,无需处理返回结果。
同步API的适用场景与性能表现
- 代码复杂度极低:同步调用直接通过
Stub::Method()发起,无需管理CompletionQueue和后台线程,逻辑直观,调试成本低。 - 性能下限足够:gRPC同步API底层基于异步机制封装,单调用的延迟与异步API相差无几。在低并发场景(如每秒数十次调用以内),完全能满足需求。
- 注意事项:如果发起调用的线程是不能阻塞的关键线程(如UI线程、实时业务线程),绝对不能直接用同步调用——哪怕返回Empty,网络请求的延迟(哪怕毫秒级)也会导致线程卡顿,影响用户体验或业务稳定性。这种情况下必须把同步调用放到独立的工作线程中执行。
异步API的优化与必要性
Google推荐异步API的核心原因是高并发下的资源利用率:异步模式可以复用少量线程处理大量RPC请求,避免同步模式下因线程阻塞导致的大量线程上下文切换开销。
针对你觉得“需要后台线程处理CompletionQueue冗余”的顾虑:
- 后台线程的实际开销极小:这个线程绝大多数时间会阻塞在
CompletionQueue::Next()调用上,仅当RPC完成时才会短暂唤醒,CPU占用可以忽略不计。 - 简化处理逻辑:因为无需处理返回结果,后台线程拿到事件后直接丢弃即可,无需额外业务逻辑。示例代码如下:
// 后台线程循环 void CompletionQueueWorker(grpc::CompletionQueue* cq) { void* tag; bool ok; // 持续处理队列事件,直到队列被关闭 while (cq->Next(&tag, &ok)) { // 无需处理结果,直接释放tag(如果分配了的话) delete static_cast<std::uintptr_t*>(tag); } } // 发起异步调用 void TriggerRpc(AsyncStub* stub, grpc::CompletionQueue* cq, const Request& req) { grpc::ClientContext ctx; google::protobuf::Empty resp; // 分配一个简单的tag(仅用于标识调用) auto tag = new std::uintptr_t(0); stub->AsyncYourMethod(&ctx, req, cq)->Finish(&resp, tag); }
最佳实践总结
- 低并发+非关键线程发起调用:优先选同步API,代码简洁,无额外维护成本,性能完全够用。
- 高并发场景:必须用异步API,后台线程的开销远低于高并发下同步模式的线程资源浪费。
- 关键线程发起调用:无论并发高低,都要用异步API(或把同步调用放到工作线程),避免阻塞关键线程影响业务。
内容的提问来源于stack exchange,提问作者PluginPenguin
相关产品推荐
相关产品推荐

