Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trying to understand why other boost asio non-grpc async work in same thread with grpc? #8

Closed
rbresalier opened this issue Nov 3, 2021 · 2 comments

Comments

@rbresalier
Copy link

I've been trying to understand how it is possible for your excellent library to be able to execute async operations (for example steady_timer) on the same main thread as grpc.

I ran the hello-world-server-cpp20.cpp in a debugger to help my understanding.

Part of my initial confusion is because I see in grpcContext.ipp/get_next_event() that it seems to be blocking only on the grpc completion queue (call to get_completion_queue()->AsyncNext()), so how could it unblock on other async events that are not grpc?

Then using the debugger I found that at the start of main() of hello-world-server-cpp20.cpp that this statement:

    boost::asio::basic_signal_set signals{grpc_context, SIGINT, SIGTERM};

spawns a 2nd ASIO thread.

Then I see that when I have another non-grpc async operation, such as steady_timer, that when the steady_timer expires it wakes up this 2nd ASIO thread somehow. Then when that wakes up somehow you get it to post a grpc alarm with immediate deadline to the grpc completion queue which unblocks the main thread and allows the handler for the steady timer to execute. Is this the proper understanding?

So would it be that any (not just steady_timer) non-grpc async completion handlers would wake up that 2nd thread which then sends an immediate grpc alarm to wake up the completion handler in the main thread? That is how you get non-grpc async completion handlers to execute?

Not knowing how boost::asio works so well, I suppose this 2nd thread is always there and wakes up the main thread, even when using basic io_context and not an overridden execution_context? Or is this 2nd thread somehow created because you have overridden the basic io_context/execution_context?

@rbresalier rbresalier changed the title Trying to understand why other boost asio async work in same thread with grpc? Trying to understand why other boost asio non-grpc async work in same thread with grpc? Nov 3, 2021
@Tradias
Copy link
Owner

Tradias commented Nov 3, 2021

You analyzed that perfectly. Boost.Asio has a mechanism called Services, which allows an IO-Object (like asio::steady_timer) to instruct an asio::execution_context to create and hold on to some "global" state that the IO-Object needs to perform its tasks. In the case of asio::steady_timer this would be an asio::io_context (and a timer thread on Windows). If you step through the debugger of the constructor of asio::steady_timer you will eventually see a call like use_service<win_iocp_io_context>(context). This call tells the GrpcContext that it should provide an asio::io_context for the timer. Since the GrpcContext is not an asio::io_context itself, it has to create one and run it on a background thread. All that happens automatically btw. by Boost.Asio, all I had to do is to inherit from asio::execution_context which is required.
So you are right that when using an asio::io_context directly with an asio::steady_timer that no such extra thread is being created. In my library you can use grpc::Alarm instead to avoid creating the extra thread.

Once the asio::steady_timer has expired it will use the GrpcExecutor to notify the GrpcContext by calling asio::dispatch. You can see in GrpcContextImplementation::add_remote_operation that this will set an immediate alarm on the GrpcContext that will wake it up so that it can call the completion handler that was passed to steady_timer.async_wait.

Some functions do not require an extra thread like asio::post, asio::dispatch, asio::execute, asio::co_spawn and so on. They instead either set the immediate alarm (if they were made from a different thread) or simply allocate an operation and put it into the GrpcContext's intrusive queue.

@Tradias
Copy link
Owner

Tradias commented Nov 8, 2021

For v1.4.0 I am planning on making this available the other way around as well. For applications that primarily use asio::io_context, e.g. because they are a webserver and only need the occasional RPC functionality, let's say as a client it would then be more efficient (and probably convienent) to write something like:

const auto stub = test::v1::Test::NewStub(grpc::CreateChannel(host, grpc::InsecureChannelCredentials()));
boost::asio::io_context io_context;
agrpc::GrpcStub grpc_stub{*stub, io_context};
co_await agrpc::request(..., grpc_stub, ...);

In which case the constructor ot agrpc::GrpcStub would ask the io_context for a agrpc::GrpcContext via the use_service mechanism. If none exists, then one will be created and run on a background thread.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants