I'm dabbling in coroutines in respect to boost::asio, and I'm confused by exception handling. Judging by the examples in the docs, it looks like any 'fail' error_code is turned into an exception - so I hopefully assumed that any exception thrown would also be propagated back to the co_spawn call. But that doesn't appear to be case:
#define BOOST_ASIO_HAS_CO_AWAIT
#define BOOST_ASIO_HAS_STD_COROUTINE
#include <iostream>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/executor_work_guard.hpp>
namespace this_coro = boost::asio::this_coro;
boost::asio::awaitable<void> async_op()
{
std::cout << "About to throw" << std::endl;
throw std::runtime_error{"Bang!"};
}
int main()
{
auto ctx = boost::asio::io_context{};
auto guard = boost::asio::make_work_guard(ctx.get_executor());
boost::asio::co_spawn(ctx, async_op, boost::asio::detached);
ctx.run();
}
If this is ran in a debugger, you can see the exception being thrown, but then it just seems to hang. Pausing the debugger shows that the ctx.run() is waiting for new work (due to the executor_work_guard). So it looks like something inside boost::asio has silently swallowed the exception.
As an experiment, I switched the async operation to use boost::asio library calls:
boost::asio::awaitable<void> async_op()
{
auto executor = co_await this_coro::executor;
auto socket = boost::asio::ip::tcp::socket{executor};
std::cout << "Starting resolve" << std::endl;
auto resolver = boost::asio::ip::tcp::resolver{executor};
const auto endpoints = co_await resolver.async_resolve("localhost",
"4444",
boost::asio::use_awaitable);
std::cout << "Starting connect (num endpoints: " << endpoints.size() << ")" << std::endl;
co_await boost::asio::async_connect(socket, endpoints, boost::asio::use_awaitable);
std::cout << "Exited" << std::endl;
}
I don't have a server running on port 4444, so this should fail immediately - and it does but silently. Pausing the debugger shows that it's stuck in epoll waiting for something (I'm on Linux).
Swapping the async_connect CompletionToken to a boost::asio::redirect_error shows that the operation is failing:
co_await boost::asio::async_connect(socket,
endpoints,
boost::asio::redirect_error(boost::asio::use_awaitable, ec));
std::cout << "Exited: " << ec.message() << std::endl;
Yields:
Starting resolve
Starting connect (num endpoints: 1)
Exited: Connection refused
So how do I propagate exceptions, and create them from error_codes, out of coroutines in boost::asio?
boost::asio::co_spawn creates a separate thread. This means that exceptions are not propagated. You can read more about this here:
Will main() catch exceptions thrown from threads?
How can I propagate exceptions between threads?
But co_spawn supports a completion handler with the signature void(std::exception_ptr, R). In your example you used boost::asio::detached which means the completion result is ignored. To propagate it simply write a custom handler.
Related
Do boost::asio c++20 coroutines support multithreading?
The boost::asio documentation examples are all single-threaded, are there any multithreaded examples?
Yes.
In Asio, if multiple threads run execution context, you don't normally even control which thread resumes your coroutine.
You can look at some of these answers that ask about how to switch executors mid-stream (controlling which strand or execution context may resume the coro):
asio How to change the executor inside an awaitable?
Switch context in coroutine with boost::asio::post
Update to the comment:
To make the c++20 coro echo server sample multi-threading you could change 2 lines:
boost::asio::io_context io_context(1);
// ...
io_context.run();
Into
boost::asio::thread_pool io_context;
// ...
io_context.join();
Since each coro is an implicit (or logical) strand, nothing else is needed. Notes:
Doing this is likely useless, unless you're doing significant work inside the coroutines, that would slow down IO multiplexing on a single thread.
In practice a single thread can easily handle 10k concurrent connections, especially with C++20 coroutines.
Note that it can be a significant performance gain to run the asio::io_context(1) with the concurrency hint, because it can avoid synchronization overhead.
When you introduce e.g. asynchronous session control or full-duplex you will have the need for an explicit strand. In the below example I show how you would make each "session" use a strand, and e.g. do graceful shutdown.
Live On Coliru
#include <boost/asio.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/experimental/awaitable_operators.hpp>
#include <iostream>
#include <list>
namespace asio = boost::asio;
namespace this_coro = asio::this_coro;
using boost::system::error_code;
using asio::ip::tcp;
using asio::detached;
using executor_type = asio::any_io_executor;
using socket_type = asio::use_awaitable_t<>::as_default_on_t<tcp::socket>; // or tcp::socket
//
using session_state = std::shared_ptr<socket_type>; // or any additional state
using handle = std::weak_ptr<session_state::element_type>;
using namespace std::string_view_literals;
using namespace asio::experimental::awaitable_operators;
asio::awaitable<void> echo_session(session_state s) {
try {
for (std::array<char, 1024> data;;) {
size_t n = co_await s->async_read_some(asio::buffer(data));
co_await async_write(*s, asio::buffer(data, n));
}
} catch (boost::system::system_error const& se) {
if (se.code() != asio::error::operation_aborted) // expecting cancellation
throw;
} catch (std::exception const& e) {
std::cout << "echo Exception: " << e.what() << std::endl;
co_return;
}
error_code ec;
co_await async_write(*s, asio::buffer("Server is shutting down\n"sv),
redirect_error(asio::use_awaitable, ec));
// std::cout << "echo shutdown: " << ec.message() << std::endl;
}
asio::awaitable<void> listener(std::list<handle>& sessions) {
auto ex = co_await this_coro::executor;
for (tcp::acceptor acceptor(ex, {tcp::v4(), 55555});;) {
session_state s = std::make_shared<socket_type>(
co_await acceptor.async_accept(make_strand(ex), asio::use_awaitable));
sessions.remove_if(std::mem_fn(&handle::expired)); // "garbage collect", optional
sessions.emplace_back(s);
co_spawn(ex, echo_session(s), detached);
}
}
int main() {
std::list<handle> handles;
asio::thread_pool io_context;
asio::signal_set signals(io_context, SIGINT, SIGTERM);
auto handler = [&handles](std::exception_ptr ep, auto result) {
try {
if (ep)
std::rethrow_exception(ep);
int signal = get<1>(result);
std::cout << "Signal: " << ::strsignal(signal) << std::endl;
for (auto h : handles)
if (auto s = h.lock()) {
// more logic could be implemented via members on a session_state struct
std::cout << "Shutting down live session " << s->remote_endpoint() << std::endl;
post(s->get_executor(), [s] { s->cancel(); });
}
} catch (std::exception const& e) {
std::cout << "Server: " << e.what() << std::endl;
}
};
co_spawn(io_context, listener(handles) || signals.async_wait(asio::use_awaitable), handler);
io_context.join();
}
Online demo, and local demo:
I would like to learn how to pass timeout timer to boost::asio::yield_context.
Let's say, in terms of Boost 1.80, there is smth like the following:
#include <boost/asio/io_context.hpp>
#include <boost/asio/spawn.hpp>
void async_func_0(boost::asio::yield_context yield) {
async_func_1(yield);
}
void async_func_1(boost::asio::yield_context) {
}
int main() {
boost::asio::io_context ioc;
boost::asio::spawn(ioc.get_executor(), &async_func_0);
ioc.run();
return 0;
}
Let's imaging that the async_func_1 is quite a burden, it is async by means of boost::coroutines (since boost::asio does not use boost::coroutines2 for some unknown reason) and it can work unpredictably long, mostly on io operations.
A good idea would be to specify the call of async_func_1 with a timeout so that if the time passed it must return whatever with an error. Let's say at the nearest use of boost::asio::yield_context within the async_func_1.
But I'm puzzled how it should be expressed in terms of boost::asio.
P.S. Just to exemplify, in Rust it would be smth like the following:
use std::time::Duration;
use futures_time::FutureExt;
async fn func_0() {
func_1().timeout(Duration::from_secs(60)).await;
}
async fn func_1() {
}
#[tokio::main]
async fn main() {
tokio::task::spawn(func_0());
}
In Asio cancellation and executors are separate concerns.
That's flexible. It also means you have to code your own timeout.
One very rough idea:
#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>
#include <iostream>
namespace asio = boost::asio;
using boost::asio::yield_context;
using namespace std::chrono_literals;
using boost::system::error_code;
static std::chrono::steady_clock::duration s_timeout = 500ms;
template <typename Token>
void async_func_1(Token token) {
error_code ec;
// emulating a long IO bound task
asio::steady_timer work(get_associated_executor(token), 1s);
work.async_wait(redirect_error(token, ec));
std::cout << "async_func_1 completion: " << ec.message() << std::endl;
}
void async_func_0(yield_context yield) {
asio::cancellation_signal cancel;
auto cyield = asio::bind_cancellation_slot(cancel.slot(), yield);
std::cout << "async_func_0 deadline at " << s_timeout / 1.0s << "s" << std::endl;
asio::steady_timer deadline(get_associated_executor(cyield), s_timeout);
deadline.async_wait([&](error_code ec) {
std::cout << "Timeout: " << ec.message() << std::endl;
if (!ec)
cancel.emit(asio::cancellation_type::terminal);
});
async_func_1(cyield);
std::cout << "async_func_0 completion" << std::endl;
}
int main(int argc, char** argv) {
if (argc>1)
s_timeout = 1ms * atoi(argv[1]);
boost::asio::io_context ioc;
spawn(ioc.get_executor(), async_func_0);
ioc.run();
}
No online compilers that accept this¹ are able to run this currently. So here's local output:
for t in 150 1500; do time ./build/sotest "$t" 2>"$t.trace"; ~/custom/superboost/libs/asio/tools/handlerviz.pl < "$t.trace" | dot -T png -o trace_$t.png; done
async_func_0 deadline at 0.15s
Timeout: Success
async_func_1 completion: Operation canceled
async_func_0 completion
real 0m0,170s
user 0m0,009s
sys 0m0,011s
async_func_0 deadline at 1.5s
async_func_1 completion: Success
async_func_0 completion
Timeout: Operation canceled
real 0m1,021s
user 0m0,011s
sys 0m0,011s
And the handler visualizations:
¹ wandbox, coliru, CE
Road From Here
You'll probably say this is cumbersome. Compared to your Rust library feature it is. To library this in Asio you could
derive your own completion token from type yield_context, adding the behaviour you want
make a composing operation (e.g. using deferred)
I'm having problems using named_mutex, which I am trying to use to determine if another instance of my application is running.
I defined a global variable:
named_mutex dssMutex{ open_or_create, "DeepSkyStacker.Mutex.UniqueID.12354687" };
In main() I then wrote:
if (!dssMutex.try_lock()) firstInstance = false;
and at the end of main() after all the catch stuff I did:
dssMutex.unlock();
The problem I have encountered is that try_lock() is returning false when this is the only instance of my program in the system (just after a reboot). I also see this in the debug log (which may just be an artefact of try_lock()):
Exception thrown at 0x00007FFB838C4FD9 in DeepSkyStacker.exe: Microsoft C++ exception: boost::interprocess::interprocess_exception at memory location 0x00007FF5FFF7EF00.
So what am I doing wrong?
Thanks
David
Three things:
you should not unlock if try_lock returned false;
you should be exception safe, which is easier with the scoped_lock helper
Boost's interprocess locking primitives are not robust mutexes. This means that if your process gets hard-terminated without unlocking, the lock will be stuck. To the best of my knowledge the implementation(s) on Windows contain a "boot time" field which serves to recover the lock after a reboot, though, so your described scenario should really not be a problem.
The Exception
The exception shown should not be a problem unless it goes unhandled. If you're using Visual Studio you can configure the debugger to break on exceptions thrown or unhandled. The best explanation for the message is that it is handled internally. The worst explanation is that you're not handling it. In that case it will explain that the lock is not released.
Note that the exception might be cause by trying to unlock after failing to try_lock?
Code Sample
Here's how I'd use a deffered scope-lock to achieve exception safety:
#include <boost/interprocess/sync/named_mutex.hpp>
#include <boost/interprocess/sync/scoped_lock.hpp>
#include <iostream>
#include <thread>
namespace bip = boost::interprocess;
using namespace std::chrono_literals;
int main(int, char** argv) {
bip::named_mutex dssMutex{bip::open_or_create, "UniqueID.12354687"};
bip::scoped_lock<bip::named_mutex> lk(dssMutex, bip::defer_lock);
bool const firstInstance = lk.try_lock();
std::cout << argv[0] << (firstInstance?" FRIST!":" SECOND") << std::flush;
std::this_thread::sleep_for(1s);
std::cout << " Bye\n" << std::flush;
}
Coliru cannot handle it but here's what that does locally:
Signal Handling
Now, as mentioned, this is still not robust, but you can make it less bad by at least handling e.g. SIGINT (what happens on POSIX when you Ctrl-C in the terminal):
#include <boost/asio.hpp>
#include <boost/interprocess/sync/named_mutex.hpp>
#include <boost/interprocess/sync/scoped_lock.hpp>
#include <iostream>
#include <thread>
namespace bip = boost::interprocess;
using namespace std::chrono_literals;
int main(int, char** argv) {
boost::asio::thread_pool ioc(1);
boost::asio::signal_set ss(ioc, SIGINT, SIGTERM);
ss.async_wait([](auto ec, int s) {
if (ec == boost::asio::error::operation_aborted)
return;
std::cerr << "signal " << s << " (" << ec.message() << ")" << std::endl;
});
bip::named_mutex dssMutex{bip::open_or_create, "UniqueID.12354687"};
bip::scoped_lock<bip::named_mutex> lk(dssMutex, bip::defer_lock);
bool const firstInstance = lk.try_lock();
std::cout << argv[0] << (firstInstance?" FRIST!":" SECOND") << std::flush;
std::this_thread::sleep_for(1s);
std::cout << " Bye\n" << std::flush;
ss.cancel();
ioc.join();
}
Now it's okay to interrupt the processes:
for a in {1..10}; do sleep "0.$RANDOM"; ./one; done&
for a in {1..10}; do sleep "0.$RANDOM"; ./two; done&
sleep 3; pkill -INT -f ./one;
sleep 5; pkill -INT -f ./two
If you look closely, the handler doesn't actually do anything now. So likely you want to make sure it cleanly shuts down main.
I do not know if this is the expected behavior of boost::asio::co_spawn (I did check the docs 1.78.0...), but if i call (e.g. co_spawn(ctx, ..., detached)) from a function, this call is async, meaning that this call does not block waiting for the completion, but returns immediately. However, if i do the same call from within another coroutine, co_spawn will block until whatever that was spawned completes. Below is the a test compiled with g++ 11.2 with boost asio 1.78.
#include <iostream>
#include <thread>
#include <chrono>
#include <coroutine>
#include <boost/asio.hpp>
#include <boost/asio/experimental/as_tuple.hpp>
using namespace boost;
using namespace boost::asio;
awaitable<void> TestCoro2(io_context& ctx) {
std::cout << "test coro1 thread id = " << std::this_thread::get_id() << std::endl;
co_return;
}
awaitable<void> TestCoro1(io_context& ctx) {
std::cout << "test coro1 thread id = " << std::this_thread::get_id() << std::endl;
std::cout << "333" << std::endl;
//co_await TestCoro2(ctx);
co_spawn(ctx, TestCoro2(ctx), detached);
std::cout << "444" << std::endl;
co_return;
}
awaitable<void> TestCoro(io_context& ctx) {
std::cout << "test coro thread id = " << std::this_thread::get_id() << std::endl;
std::cout << "111" << std::endl;
co_spawn(ctx.get_executor(), TestCoro1(ctx), detached);
std::cout << "222" << std::endl;
co_return;
}
void Test1() {
io_context ctx;
auto work = require(ctx.get_executor(), execution::outstanding_work.tracked);
std::cout << "before" << std::endl;
co_spawn(ctx.get_executor(), TestCoro(ctx), detached);
std::cout << "after" << std::endl;
ctx.run();
}
int main() {
Test1();
return 0;
}
In the example above i had not yet called ctx.run() when spawking a coro... still semantics, I would expect, to be similar...
My understanding was that first it will schedule and return, and a currently running coroutine will proceed, however i guess i was wrong. I do understand that i can also just wrap this co_spawn into a post... but i'm a bit confused on the difference in behavior...
Is this the expected behavior?
thanks!
VK
The problem is that you use the same context for all of your coroutines. co_spawn internally uses dispatch() to ensure the coroutine starts in the desired context. dispatch() calls the token synchronously if the target context is the same as the current one. So the coroutine is executed synchronously in such case, at least until the first suspension point (some co_await).
You can insert this line at the beginning of your coroutine to ensure it is always scheduled instead of being called synchronously, even when called from the same context:
co_await asio::post(ctx, asio::use_awaitable);
For some time I have been trying to use std::thread, and in my project i wanted to make sure that the threads are not making one thing couple times at once, that's why i am trying to make a simple project that has something like "check" if thread is done, and then start again
#include <future>
#include <thread>
#include <chrono>
#include <iostream>
using namespace std::chrono_literals;
void Thing()
{
std::this_thread::sleep_for(3s);
}
int main()
{
std::packaged_task<void()> task(Thing);
auto future = task.get_future();
std::thread t(std::move(task));
while (true) {
auto status = future.wait_for(0ms);
if (status != std::future_status::ready)
{
std::cout << "not yet" << std::endl;
}
else
{
t.join();
std::cout << "Join()" << std::endl;
}
std::this_thread::sleep_for(300ms);
}
}
using this code i have error at line with std::cout << "Join()" << std::endl; and the error says: Unhandled exception at 0x7632A842 in dasd.exe: Microsoft C++ exception: std::system_error at memory location 0x00AFF8D4.
this error is comes out when the thread is ready, and t.join() is called.
output of this project:
not yet
...
not yet
Join()
Thank You in advance
As you can see https://en.cppreference.com/w/cpp/thread/thread/join
join has as post condition
joinable() is false
and in error condition
invalid_argument if joinable() is false
So you cannot call it twice as you do.
You probably want to break the loop once you call join or rewrite your loop such as:
while (future.wait_for(300ms) != std::future_status::ready) {
std::cout << "not yet" << std::endl;
}
t.join();
std::cout << "Join()" << std::endl;
Actually, you don't need a while loop there. Instead, you could simply call join.
What join does is to wait for the thread to finish its job. After the thread finishes its job, it exits and cleans the stack and the second call to join doesn't make sense at all.
I also would suggest using std::async in case you want an async function that also returns a value.