可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
using Yield = asio::yield_context; using boost::system::error_code; int Func(Yield yield) { error_code ec; asio::detail::async_result_init init(yield[ec]); std::thread th(std::bind(Process, init.handler)); int result = init.result.get(); //
How to implement Process
so that Func
will resumed in the context of the strand that Func
was originally spawned on?
回答1:
Boost.Asio uses a helper function, asio_handler_invoke
, to provide a customization point for invocation strategies. For example, when a Handler has been wrapped by a strand
, the invocation strategy will cause the handler to be dispatched through the strand
upon invocation. As noted in the documentation, asio_handler_invoke
should be invoked via argument-dependent lookup.
using boost::asio::asio_handler_invoke; asio_handler_invoke(nullary_functor, &handler);
For stackful coroutines, there are various important details to take into consideration when yielding the coroutine and when invoking the handler_type
associated with a yield_context
to resume the coroutine:
- If code is currently running in the coroutine, then it is within the
strand
associated with the coroutine. Essentially, a simple handler is wrapped by the strand
that resumes the coroutine, causing execution to jump to the coroutine, blocking the handler currently in the strand
. When the coroutine yields, execution jumps back to the strand
handler, allowing it to complete. - While
spawn()
adds work to the io_service
(a handler that will start and jump to the coroutine), the coroutine itself is not work. To prevent the io_service
event loop from ending while a coroutine is outstanding, it may be necessary to add work to the io_service
before yielding. - Stackful coroutines use a
strand
to help guarantee the coroutine yields before resume is invoked. Asio 1.10.6 / Boost 1.58 enabled being able to safely invoke the completion handler from within the initiating function. Prior versions required that the completion handler was not invoked from within the initiating function, as its invocation strategy would dispatch()
, causing the coroutine to attempt resumption before being suspended.
Here is a complete example that accounts for these details:
#include // std::cout, std::endl #include // std::chrono::seconds #include // std::bind #include // std::thread #include // std::forward #include #include template using handler_type_t = typename boost::asio::handler_type::type; template using async_result = boost::asio::async_result; /// @brief Helper type used to initialize the asnyc_result with the handler. template struct async_completion { typedef handler_type_t handler_type; async_completion(CompletionToken&& token) : handler(std::forward(token)), result(handler) {} handler_type handler; async_result result; }; template typename async_result >::type async_func(CompletionToken&& token, boost::asio::io_service& io_service) { // The coroutine itself is not work, so guarantee the io_service has // work. boost::asio::io_service::work work(io_service); // Initialize the async completion handler and result. async_completion completion( std::forward(token)); auto handler = completion.handler; std::cout (yield, io_service); std::cout
Output:
Running Spawning thread Resume coroutine Suspend coroutine Got: 42 Finish
For much more details, please consider reading Library Foundations for Asynchronous Operations. It provides much greater detail into the composition of asynchronous operations, how Signature
affects async_result
, and the overall design of async_result
, handler_type
, and async_completion
.
回答2:
Here's an updated example for Boost 1.66.0 based on Tanner's great answer:
#include // std::cout, std::endl #include // std::chrono::seconds #include // std::bind #include // std::thread #include // std::forward #include #include template auto async_add_one(CompletionToken token, int value) { // Initialize the async completion handler and result // Careful to make sure token is a copy, as completion's handler takes a reference using completion_type = boost::asio::async_completion; completion_type completion{ token }; std::cout (yield, 0); std::cout (yield, 41); std::cout
Output:
Running Spawning thread Resume coroutine Suspend coroutine Got: 1 Spawning thread Resume coroutine Suspend coroutine Got: 42 Finish
Remarks:
- Greatly leverages Tanner's answer
- Prefer network TS naming (e.g, io_context)
- boost::asio provides an async_completion class which encapsulates the handler and async_result. Careful as the handler takes a reference to the CompletionToken, which is why the token is now explicitly copied. This is because yielding via async_result (
completion.result.get
) will have the associated CompletionToken give up its underlying strong reference. Which can eventually lead to unexpected early termination of the coroutine. - Make it clear that a separate
using boost::asio::asio_handler_invoke
statement is really important. An explicit call can prevent the correct overload from being invoked.
-
I'll also mention that our application ended up with two io_context's which a coroutine may interact with. Specifically one context for I/O bound work, the other for CPU. Using an explicit strand with boost::asio::spawn
ended up giving us well defined control over the context in which the coroutine would run/resume. This helped us avoid sporadic BOOST_ASSERT( ! is_running() ) failures.
Creating a coroutine with an explicit strand:
auto strand = std::make_shared(io_context.get_executor()); boost::asio::spawn( *strand, [&io_context, strand](yield_context_type yield) { // coroutine } );
with invocation explicitly dispatching to the strand (multi io_context world):
boost::asio::dispatch(*strand, [handler = completion.completion_handler, value] { using boost::asio::asio_handler_invoke; asio_handler_invoke(std::bind(handler, value), &handler); });
-
We also found that using future's in the async_result signature allows for exception propagation back to the coroutine on resumption.
using bound_function = void(std::future); using completion_type = boost::asio::async_completion;
with yield being:
auto future = completion.result.get(); return future.get(); // may rethrow exception in your coroutine's context
回答3:
You are complicating things by creating threads out of the executor framework provided by Boost Asio.
For this reason you shouldn't assume that what you want is possible. I strongly suggest just adding more threads to the io_service
and letting it manage the strands for you.
回答4:
using CallbackHandler = boost::asio::handler_type::type; void Process(CallbackHandler handler) { int the_result = 81; boost::asio::detail::asio_handler_invoke( std::bind(handler, error_code(), the_result), &handler); }
Hinted by @sehe, I made the above working solution. But I am not sure if this is the right/idiomatic/best way to do that. Welcome to comment/edit this answer.