asio-grpc
asio-grpc copied to clipboard
Asynchronous gRPC with Asio/unified executors
asio-grpc
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
- Asio ExecutionContext compatible wrapper around grpc::CompletionQueue
- Executor and Networking TS requirements fulfilling associated executor
- Support for all RPC types: unary, client-streaming, server-streaming and bidirectional-streaming with any mix of Asio CompletionToken as well as TypedSender, including allocator customization
- Support for asynchronously waiting for grpc::Alarms including cancellation through cancellation_slots and StopTokens
- Initial support for
std::execution
concepts through libunifex and Asio: schedule, connect, submit, scheduler, typed_sender and more - Support for generic gRPC clients and servers (aka. proxies)
- Experimental support for Rust/Golang select-style programming with the help of cancellation safety
- No-Boost version with standalone Asio
- No-Asio version with libunifex
- CMake function to generate gRPC source files: asio_grpc_protobuf_generate
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
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.
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.