I have read this question and tried to replicate the answer with the following code:
#include <iostream>
#include <syncstream>
#include <thread>
#include <coroutine>
#include <boost/asio.hpp>
#include <boost/asio/experimental/as_single.hpp>
#include <boost/bind/bind.hpp>
#include <boost/thread/thread.hpp>
inline std::osyncstream tout() {
auto hash = std::hash<std::thread::id>{}(std::this_thread::get_id());
return std::osyncstream(std::cout) << "T" << hash << " ";
}
namespace asio = boost::asio;
asio::awaitable<void> mainCo(asio::io_context &appIO, asio::io_context &prodIO) {
// the thread should also change when using the IO contexts directly.
auto astrand = asio::io_context::strand{appIO};
auto pstrand = asio::io_context::strand{prodIO};
tout() << "MC on APPIO" << std::endl;
co_await asio::post(pstrand, asio::use_awaitable);
tout() << "MC on PRODIO" << std::endl;
co_await asio::post(astrand, asio::use_awaitable);
tout() << "MC on APPIO" << std::endl;
co_await asio::post(pstrand, asio::use_awaitable);
tout() << "MC on PRODIO" << std::endl;
co_await asio::post(pstrand, asio::use_awaitable); // nop - no operation because we are already on the correct execution_context
tout() << "MC on PRODIO" << std::endl;
co_await asio::post(astrand, asio::use_awaitable);
tout() << "MC on APPIO" << std::endl;
}
int main() {
asio::io_context prodIO;
boost::thread prodThread;
{
// ensure the producer io context doesn't exit
auto prodWork = asio::make_work_guard(prodIO);
prodThread = boost::thread{[&prodIO] {
tout() << "ProdThread run start" << std::endl;
prodIO.run(); // if this call is removed the mainCo is stuck as expected
tout() << "ProdThread run done" << std::endl;
}};
asio::io_context appIO;
asio::co_spawn(appIO, mainCo(appIO, prodIO), asio::detached);
tout() << "MainThread run start" << std::endl;
appIO.run();
tout() << "MainThread run done" << std::endl;
}
prodThread.join();
return 42;
}
Current output:
/tmp/tmp.wz38MWkttM/cmake-build-debug-remote-host/CoroContextSwitching
T14386720116392206644 MainThread run start
T8726023523478668610 ProdThread run start
T14386720116392206644 MC on APPIO
T14386720116392206644 MC on PRODIO
T14386720116392206644 MC on APPIO
T14386720116392206644 MC on PRODIO
T14386720116392206644 MC on PRODIO
T14386720116392206644 MC on APPIO
T14386720116392206644 MainThread run done
T8726023523478668610 ProdThread run done
Process finished with exit code 42
Expected output:
/tmp/tmp.wz38MWkttM/cmake-build-debug-remote-host/CoroContextSwitching
T14386720116392206644 MainThread run start
T8726023523478668610 ProdThread run start
T14386720116392206644 MC on APPIO
T8726023523478668610 MC on PRODIO
T14386720116392206644 MC on APPIO
T8726023523478668610 MC on PRODIO
T8726023523478668610 MC on PRODIO
T14386720116392206644 MC on APPIO
T14386720116392206644 MainThread run done
T8726023523478668610 ProdThread run done
Process finished with exit code 42
I expect the thread id to change according to the cout statements. However all cout statements are executed on the MainThread.
How can I get the desired behaviour?
Edit 2:
The original question still stands this justs adds more information to the problem.
It seems like asio::use_awaitable
is binding a default executor from somewhere.
Is there a documented default?
With the following edited function I can achieve what I want:
asio::awaitable<void> mainCo(asio::io_context &appIO, asio::io_context &prodIO) {
// the thread should also change when using the IO contexts directly.
auto astrand = asio::io_context::strand{appIO};
auto pstrand = asio::io_context::strand{prodIO};
tout() << "MC on APPIO" << std::endl;
co_await asio::post(pstrand, asio::bind_executor(pstrand, asio::use_awaitable)); // so use_awaitable is binding a default executor from somewhere.
tout() << "MC on PRODIO" << std::endl;
co_await asio::post(astrand, asio::use_awaitable);
tout() << "MC on APPIO" << std::endl;
co_await asio::post(pstrand, asio::bind_executor(pstrand, asio::use_awaitable));
tout() << "MC on PRODIO" << std::endl;
co_await asio::post(pstrand, asio::bind_executor(pstrand, asio::use_awaitable)); // nop - no operation because we are already on the correct execution_context
tout() << "MC on PRODIO" << std::endl;
co_await asio::post(astrand, asio::use_awaitable);
tout() << "MC on APPIO" << std::endl;
co_await asio::post(astrand, /* the first parameter in post doesn't even matter (can be astrand, pstrand, appIO, prodIO) same result */
asio::bind_executor(pstrand, asio::use_awaitable));
tout() << "MC on PRODIO" << std::endl;
co_await asio::post(astrand, asio::use_awaitable);
tout() << "MC on APPIO" << std::endl;
}
HOWEVER, it seems that this is not the proper way to switch executor as the first argument of asio::post
doesn't matter. So what is the correct way to do this?
Edit: The question was closed and pointed me to this does boost::asio co_spawn create an actual thread?. I am aware that co_spawn does not spawn a new thread. That's why I spawn a new thread myself called prodThread. I expect the execution_context to switch after awaiting the post statements. The linked question does not answer my question.
pstrand
is doing no work it always falls back to the main thread? I'm just guessing here. I think that is what I'm seeing by tracing this. Likeptstrand
is just waiting? – Chrissiepstrand
waiting is expected. It waits until it gets a 'time slot' on theprodIO
. But usually withasio::post
the handler is invoked on the specified executor unless the completion token is bound to a different executor. What is confusing me is that in this case the default executor ofasio::use_awaitable
is not what I would expect. – Kellen