asio-grpc icon indicating copy to clipboard operation
asio-grpc copied to clipboard

Asynchronous gRPC with Asio/unified executors

asio-grpc

Reliability Rating Reliability Rating vcpkg conan hunter

An Executor, Networking TS and std::execution interface to grpc::CompletionQueue for writing asynchronous gRPC clients and servers using C++20 coroutines, Boost.Coroutines, Asio's stackless coroutines, callbacks, sender/receiver and more.

Features

Example

  • Client side 'hello world':

helloworld::Greeter::Stub stub{grpc::CreateChannel(host, grpc::InsecureChannelCredentials())};
agrpc::GrpcContext grpc_context{std::make_unique<grpc::CompletionQueue>()};

asio::co_spawn(
    grpc_context,
    [&]() -> asio::awaitable<void>
    {
        grpc::ClientContext client_context;
        helloworld::HelloRequest request;
        request.set_name("world");
        const auto reader =
            agrpc::request(&helloworld::Greeter::Stub::AsyncSayHello, stub, client_context, request, grpc_context);
        helloworld::HelloReply response;
        co_await agrpc::finish(reader, response, status, asio::use_awaitable);
    },
    asio::detached);

grpc_context.run();

snippet source | anchor

More examples for things like streaming RPCs, double-buffered file transfer with io_uring, libunifex-based coroutines, sharing a thread with an io_context and generic clients/servers can be found in the example directory. Even more examples can be found in another repository.

Requirements

Tested by CI:

  • CMake 3.16.3 (min. 3.14)
  • gRPC 1.46.3, 1.16.1 (older versions might work as well)
  • Boost 1.79.0 (min. 1.74.0)
  • Standalone Asio 1.17.0 (min. 1.17.0)
  • libunifex 2022-02-09
  • MSVC 19.32 (Visual Studio 17 2022)
  • GCC 8.4.0, 10.3.0, 11.1.0
  • Clang 10.0.0, 12.0.0
  • AppleClang 13.0.0.13000029
  • C++17 and C++20

For MSVC compilers and asio-grpc before v1.6.0 the following compile definitions need to be set:

BOOST_ASIO_HAS_DEDUCED_REQUIRE_MEMBER_TRAIT
BOOST_ASIO_HAS_DEDUCED_EXECUTE_MEMBER_TRAIT
BOOST_ASIO_HAS_DEDUCED_EQUALITY_COMPARABLE_TRAIT
BOOST_ASIO_HAS_DEDUCED_QUERY_MEMBER_TRAIT
BOOST_ASIO_HAS_DEDUCED_QUERY_STATIC_CONSTEXPR_MEMBER_TRAIT
BOOST_ASIO_HAS_DEDUCED_PREFER_MEMBER_TRAIT

When using standalone Asio then omit the BOOST_ prefix.

Usage

The library can be added to a CMake project using either add_subdirectory or find_package. Once set up, include the individual headers from the agrpc/ directory or the convenience header:

#include <agrpc/asio_grpc.hpp>
As a subdirectory

Clone the repository into a subdirectory of your CMake project. Then add it and link it to your target.

Using Boost.Asio:

find_package(gRPC)
find_package(Boost)
add_subdirectory(/path/to/repository/root)
target_link_libraries(your_app PUBLIC gRPC::grpc++_unsecure asio-grpc::asio-grpc Boost::headers)

Or using standalone Asio:

find_package(gRPC)
find_package(asio)
add_subdirectory(/path/to/repository/root)
target_link_libraries(your_app PUBLIC gRPC::grpc++_unsecure asio-grpc::asio-grpc-standalone-asio asio::asio)

Or using libunifex:

find_package(gRPC)
find_package(unifex)
add_subdirectory(/path/to/repository/root)
target_link_libraries(your_app PUBLIC gRPC::grpc++_unsecure asio-grpc::asio-grpc-unifex unifex::unifex)
As a CMake package

Clone the repository and install it.

cmake -B build -DCMAKE_INSTALL_PREFIX=/desired/installation/directory .
cmake --build build --target install

Locate it and link it to your target.

Using Boost.Asio:

# Make sure CMAKE_PREFIX_PATH contains /desired/installation/directory
find_package(asio-grpc)
target_link_libraries(your_app PUBLIC asio-grpc::asio-grpc)

Or using standalone Asio:

# Make sure CMAKE_PREFIX_PATH contains /desired/installation/directory
find_package(asio-grpc)
target_link_libraries(your_app PUBLIC asio-grpc::asio-grpc-standalone-asio)

Or using libunifex:

# Make sure CMAKE_PREFIX_PATH contains /desired/installation/directory
find_package(asio-grpc)
target_link_libraries(your_app PUBLIC asio-grpc::asio-grpc-unifex)
Using vcpkg

Add asio-grpc to the dependencies inside your vcpkg.json:

{
    "name": "your_app",
    "version": "0.1.0",
    "dependencies": [
        "asio-grpc",
        // To use the Boost.Asio backend add
        // "boost-asio",
        // To use the standalone Asio backend add
        // "asio",
        // To use the libunifex backend add
        // "libunifex"
    ]
}

Locate asio-grpc and link it to your target in your CMakeLists.txt:

find_package(asio-grpc)
# Using the Boost.Asio backend
target_link_libraries(your_app PUBLIC asio-grpc::asio-grpc)
# Or use the standalone Asio backend
#target_link_libraries(your_app PUBLIC asio-grpc::asio-grpc-standalone-asio)
# Or use the libunifex backend
#target_link_libraries(your_app PUBLIC asio-grpc::asio-grpc-unifex)

Available features

boost-container - Use Boost.Container instead of <memory_resource>.

See selecting-library-features to learn how to select features with vcpkg.

Using Hunter

See asio-grpc's documentation on the Hunter website: https://hunter.readthedocs.io/en/latest/packages/pkg/asio-grpc.html.

Using conan

Please refer to the conan documentation on how to use packages. The recipe in conan-center is called asio-grpc/2.0.0.
If you are using conan's CMake generator then link with asio-grpc::asio-grpc independent of the backend that you choose:

find_package(asio-grpc)
target_link_libraries(your_app PUBLIC asio-grpc::asio-grpc)

Available options

backend - One of "boost" for Boost.Asio, "asio" for standalone Asio or "unifex" for libunifex.

use_boost_container - "True" to use Boost.Container instead of <memory_resource>.

CMake Options

ASIO_GRPC_USE_BOOST_CONTAINER - Use Boost.Container instead of <memory_resource>.

ASIO_GRPC_DISABLE_AUTOLINK - Set before using find_package(asio-grpc) to prevent asio-grpcConfig.cmake from finding and setting up interface link libraries.

Performance

asio-grpc is part of grpc_bench. Head over there to compare its performance against other libraries and languages.

Results from the helloworld unary RPC
Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz, Linux, GCC 12.1.0, Boost 1.79.0, gRPC 1.48.0, asio-grpc v2.0.0, jemalloc 5.2.1
Request scenario: string_100B

Results

1 CPU server

name req/s avg. latency 90 % in 95 % in 99 % in avg. cpu avg. memory
go_grpc 48414 19.93 ms 30.18 ms 33.47 ms 39.95 ms 100.86% 25.06 MiB
rust_thruster_mt 45317 21.87 ms 9.70 ms 11.11 ms 641.64 ms 102.59% 11.67 MiB
rust_tonic_mt 40461 24.54 ms 10.77 ms 11.74 ms 655.83 ms 102.5% 13.12 MiB
cpp_grpc_mt 35571 27.98 ms 29.67 ms 30.08 ms 31.35 ms 103.07% 5.1 MiB
rust_grpcio 35451 28.08 ms 29.79 ms 30.35 ms 31.21 ms 102.48% 18.08 MiB
cpp_asio_grpc_unifex 33908 29.36 ms 31.24 ms 31.68 ms 32.93 ms 103.81% 5.52 MiB
cpp_asio_grpc_callback 33155 30.03 ms 31.93 ms 32.43 ms 34.03 ms 103.35% 6.46 MiB
cpp_asio_grpc_coroutine 31175 31.95 ms 34.03 ms 34.56 ms 36.05 ms 101.82% 5.07 MiB
cpp_asio_grpc_io_context_coro 29658 33.58 ms 35.93 ms 36.38 ms 37.71 ms 77.96% 5.62 MiB
cpp_grpc_callback 10057 91.56 ms 137.04 ms 166.93 ms 179.02 ms 101.51% 47.08 MiB

2 CPU server

name req/s avg. latency 90 % in 95 % in 99 % in avg. cpu avg. memory
cpp_asio_grpc_unifex 81705 10.40 ms 15.91 ms 18.98 ms 27.27 ms 210.83% 26.59 MiB
cpp_grpc_mt 81053 10.41 ms 16.32 ms 19.70 ms 28.66 ms 206.76% 26.28 MiB
cpp_asio_grpc_callback 79425 10.73 ms 16.54 ms 19.81 ms 28.51 ms 208.81% 23.9 MiB
cpp_asio_grpc_coroutine 73646 11.81 ms 19.23 ms 22.44 ms 30.80 ms 210.83% 24.79 MiB
cpp_asio_grpc_io_context_coro 70568 12.41 ms 20.43 ms 24.03 ms 33.61 ms 160.64% 24.76 MiB
go_grpc 65692 13.24 ms 20.63 ms 23.63 ms 30.85 ms 194.68% 25.44 MiB
rust_thruster_mt 65361 13.87 ms 36.33 ms 59.45 ms 81.96 ms 193.87% 13.4 MiB
cpp_grpc_callback 64623 12.64 ms 24.25 ms 29.55 ms 42.64 ms 206.96% 55.59 MiB
rust_tonic_mt 58563 16.01 ms 41.97 ms 63.81 ms 98.95 ms 201.64% 15.45 MiB
rust_grpcio 57612 16.40 ms 24.81 ms 27.37 ms 32.15 ms 215.98% 29.94 MiB

Documentation

Documentation

The main workhorses of this library are the agrpc::GrpcContext and its executor_type - agrpc::GrpcExecutor.

The agrpc::GrpcContext implements asio::execution_context and can be used as an argument to Asio functions that expect an ExecutionContext like asio::spawn.

Likewise, the agrpc::GrpcExecutor satisfies the Executor and Networking TS and Scheduler requirements and can therefore be used in places where Asio/libunifex expects an Executor or Scheduler.

The API for RPCs is modeled closely after the asynchronous, tag-based API of gRPC. As an example, the equivalent for grpc::ClientAsyncReader<helloworld::HelloReply>.Read(helloworld::HelloReply*, void*) would be agrpc::read(grpc::ClientAsyncReader<helloworld::HelloReply>&, helloworld::HelloReply&, CompletionToken).

Instead of the void* tag in the gRPC API the functions in this library expect a CompletionToken. Asio comes with several CompletionTokens already: C++20 coroutine, stackless coroutine, callback and Boost.Coroutine. There is also a special token created by agrpc::use_sender(scheduler) that causes RPC functions to return a TypedSender.

If you are interested in learning more about the implementation details of this library then check out this blog article.

Getting started

Getting started

Start by creating a agrpc::GrpcContext.

For servers and clients:

grpc::ServerBuilder builder;
agrpc::GrpcContext grpc_context{builder.AddCompletionQueue()};

snippet source | anchor

For clients only:

agrpc::GrpcContext grpc_context{std::make_unique<grpc::CompletionQueue>()};

snippet source | anchor

Add some work to the grpc_context and run it. Make sure to shutdown the server before destructing the grpc_context. Also destruct the grpc_context before destructing the server. A grpc_context can only be run on one thread at a time.

grpc_context.run();
server->Shutdown();
}  // grpc_context is destructed here before the server

snippet source | anchor

It might also be helpful to create a work guard before running the agrpc::GrpcContext to prevent grpc_context.run() from returning early.

std::optional guard{asio::require(grpc_context.get_executor(), asio::execution::outstanding_work_t::tracked)};

snippet source | anchor

Where to go from here?

Check out the examples and the documentation.

What users are saying

Asio-grpc abstracts away the implementation details of asynchronous grpc handling: crafting working code is easier, faster, less prone to errors and considerably more fun. At 3YOURMIND we reliably use asio-grpc in production since its very first release, allowing our developers to effortlessly implement low-latency/high-throughput asynchronous data transfer in time critical applications.

@3YOURMIND

Our project is a real-time distributed motion capture system that uses your framework to stream data back and forward between multiple machines. Previously I have tried to build a bidirectional streaming framework from scratch using only gRPC. However, it's not maintainable and error-prone due to a large amount of service and streaming code. As a developer whose experienced both raw grpc and asio-grpc, I can tell that your framework is a real a game-changer for writing grpc code in C++. It has made my life much easier. I really appreciate the effort you have put into this project and your superior skills in designing c++ template code.

@khanhha