How to correctly migrate awaitables between different execution contexts?

3 days ago 4
ARTICLE AD BOX

Indeed, the solution would be to avoid destroying the execution contexts while another thread still refers to it.

In your case, adding tp.join() isn't quite enough:

io.poll_one(); tp.join();

You also need to ensure that the io_context is torn down before the thread pool:

asio::thread_pool tp(1); asio::io_context io;

Here's a simple demo program that works: Live On Compiler Explorer

#include <boost/asio.hpp> #include <print> namespace asio = boost::asio; static std::atomic_int tid_gen; thread_local int const tid = tid_gen++; inline void out(auto const& msg) { std::println("T{:x} {}", tid, msg); } asio::awaitable<void> f(auto strand) { out("IO Context Executor"); assert(!strand.running_in_this_thread()); co_await dispatch(bind_executor(strand, asio::deferred)); out("Strand Executor"); assert(strand.running_in_this_thread()); co_await dispatch(asio::deferred); out("Back on IO Context Executor"); assert(!strand.running_in_this_thread()); } int main() { asio::thread_pool tp(1); asio::io_context io; co_spawn(io, f(make_strand(tp)), asio::detached); io.poll_one(); tp.join(); }

Printing

T0 IO Context Executor T1 Strand Executor

So, you can see the coro is resumed only once on the io context.


Is This Okay

Yes. It's completely fine, though as you found it can be tricky to correctly manage lifetimes.

Let me respond to a claim in your question:

it absolutely kills standard, beautiful and simple asio event loop termination via call to stop(), mentioned in almost every single asio example and requires much more complex and thorough termination

That's... painful. I understand that small, synthetic examples in Asio might use the blunt force of stop() for brevity.

However, in any larger scale setting, using stop() to achieve shutdown is an anti-pattern. I hold this opinion because it basically guarantees you will not have any graceful shutdown of any of your async sessions/operation chains.

My suggestion is to always use graceful shutdown, and only as a last resort to use stop(). Note that then you will have to manually ensure that no thread will be running any of the execution contexts at the point of their destruction.

I think you indicate that you (correctly) understand that this implies you will never be able to cancel a blocking operation, even if it is runaway/deadlocked.

In that case, the puritan recourse would be to aim for abnormal program termination, though it is up to you what you value more in the face of Byzantine failure (or programmer error, obvsiouly).

Read Entire Article