How resume the execution of a stackful coroutine in the context of its strand?

前端 未结 4 1632
情深已故
情深已故 2020-12-15 02:07
using Yield = asio::yield_context;
using boost::system::error_code;
int Func(Yield yield) {
  error_code ec;
  asio::detail::async_result_init

        
4条回答
  •  春和景丽
    2020-12-15 02:30

    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 << "Spawning thread" << std::endl;
        std::thread([handler = completion.completion_handler, value]() {
            // The handler will be dispatched to the coroutine's strand.
            // As this thread is not running within the strand, the handler
            // will actually be posted, guaranteeing that yield will occur
            // before the resume.
            std::cout << "Resume coroutine" << std::endl;
    
            // separate using statement is important
            // as asio_handler_invoke is overloaded based on handler's type
            using boost::asio::asio_handler_invoke;
            asio_handler_invoke(std::bind(handler, value + 1), &handler);
        }).detach();
    
        // Demonstrate that the handler is serialized through the strand by
        // allowing the thread to run before suspending this coroutine.
        std::this_thread::sleep_for(std::chrono::seconds(2));
    
        // Yield the coroutine.  When this yields, execution transfers back to
        // a handler that is currently in the strand.  The handler will complete
        // allowing other handlers that have been posted to the strand to run.
        std::cout << "Suspend coroutine" << std::endl;
        return completion.result.get();
    }
    
    int main() {
        boost::asio::io_context io_context;
    
        boost::asio::spawn(
            io_context,
            [&io_context](boost::asio::yield_context yield) {
                // Here is your coroutine
    
                // The coroutine itself is not work, so guarantee the io_context
                // has work while the coroutine is running
                const auto work = boost::asio::make_work_guard(io_context);
    
                // add one to zero
                const auto result = async_add_one(yield, 0);
                std::cout << "Got: " << result << std::endl; // Got: 1
    
                // add one to one forty one
                const auto result2 = async_add_one(yield, 41);
                std::cout << "Got: " << result2 << std::endl; // Got: 42
            }
        );
    
        std::cout << "Running" << std::endl;
        io_context.run();
        std::cout << "Finish" << std::endl;
    }
    

    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
    

提交回复
热议问题