How can I get a future from boost::asio::post? - c++

I am using Boost 1.66.0, in which asio has built-in support for interoperating with futures (and for some time now). The examples I've seen online indicate how to achieve this cleanly when using networking functions such as async_read, async_read_some, etc. That is done by providing boost::asio::use_future in place of the completion handler, which causes the initiating function to return a future as expected.
What kind of object do I need to provide or wrap my function in to get the same behavior from boost::asio::post?
My purpose for posting the work is to execute it in the context of a strand but otherwise wait for the work to complete, so I can get the behavior I want doing:
std::packaged_task<void()> task( [] { std::cout << "Hello world\n"; } );
auto f = task.get_future();
boost::asio::post(
boost::asio::bind_executor(
strand_, std::move( task ) ) );
f.wait();
but according to the boost::asio documentation, the return type for boost::asio::post is deduced in the same way as for functions like boost::asio::async_read, so I feel like there has to be a nicer way that can avoid the intermediate packaged_task. Unlike async_read there is no "other work" to be done by post so providing just boost::asio::use_future doesn't makes sense, but we could define an async_result trait to get the same behavior for post.
Is there a wrapper or something that has the necessary traits defined to get the behavior I want or do I need to define it myself?

UPDATE: With more recent boost, use this much simpler answer
What kind of object do I need to provide or wrap my function in to get the same behavior from boost::asio::post?
You can't. post is a void operation. So the only option to achieve it with post is to use a packaged-task, really.
The Real Question
It was hidden in the part "how to get the same behaviour" (just not from post):
template <typename Token>
auto async_meaning_of_life(bool success, Token&& token)
{
using result_type = typename asio::async_result<std::decay_t<Token>, void(error_code, int)>;
typename result_type::completion_handler_type handler(std::forward<Token>(token));
result_type result(handler);
if (success)
handler(error_code{}, 42);
else
handler(asio::error::operation_aborted, 0);
return result.get ();
}
You can use it with a future:
std::future<int> f = async_meaning_of_life(true, asio::use_future);
std::cout << f.get() << "\n";
Or you can just use a handler:
async_meaning_of_life(true, [](error_code ec, int i) {
std::cout << i << " (" << ec.message() << ")\n";
});
Simple demo: Live On Coliru
Extended Demo
The same mechanism extends to supporting coroutines (with or without exceptions). There's a slightly different dance with async_result for Asio pre-boost 1.66.0.
See all the different forms together here:
How to set error_code to asio::yield_context
Live On Coliru
#define BOOST_COROUTINES_NO_DEPRECATION_WARNING
#include <iostream>
#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/use_future.hpp>
using boost::system::error_code;
namespace asio = boost::asio;
template <typename Token>
auto async_meaning_of_life(bool success, Token&& token)
{
#if BOOST_VERSION >= 106600
using result_type = typename asio::async_result<std::decay_t<Token>, void(error_code, int)>;
typename result_type::completion_handler_type handler(std::forward<Token>(token));
result_type result(handler);
#else
typename asio::handler_type<Token, void(error_code, int)>::type
handler(std::forward<Token>(token));
asio::async_result<decltype (handler)> result (handler);
#endif
if (success)
handler(error_code{}, 42);
else
handler(asio::error::operation_aborted, 0);
return result.get ();
}
void using_yield_ec(asio::yield_context yield) {
for (bool success : { true, false }) {
boost::system::error_code ec;
auto answer = async_meaning_of_life(success, yield[ec]);
std::cout << __FUNCTION__ << ": Result: " << ec.message() << "\n";
std::cout << __FUNCTION__ << ": Answer: " << answer << "\n";
}
}
void using_yield_catch(asio::yield_context yield) {
for (bool success : { true, false })
try {
auto answer = async_meaning_of_life(success, yield);
std::cout << __FUNCTION__ << ": Answer: " << answer << "\n";
} catch(boost::system::system_error const& e) {
std::cout << __FUNCTION__ << ": Caught: " << e.code().message() << "\n";
}
}
void using_future() {
for (bool success : { true, false })
try {
auto answer = async_meaning_of_life(success, asio::use_future);
std::cout << __FUNCTION__ << ": Answer: " << answer.get() << "\n";
} catch(boost::system::system_error const& e) {
std::cout << __FUNCTION__ << ": Caught: " << e.code().message() << "\n";
}
}
void using_handler() {
for (bool success : { true, false })
async_meaning_of_life(success, [](error_code ec, int answer) {
std::cout << "using_handler: Result: " << ec.message() << "\n";
std::cout << "using_handler: Answer: " << answer << "\n";
});
}
int main() {
asio::io_service svc;
spawn(svc, using_yield_ec);
spawn(svc, using_yield_catch);
std::thread work([] {
using_future();
using_handler();
});
svc.run();
work.join();
}
Prints
using_yield_ec: Result: Success
using_yield_ec: Answer: 42
using_yield_ec: Result: Operation canceled
using_yield_ec: Answer: 0
using_yield_catch: Answer: 42
using_future: Answer: 42
using_yield_catch: Caught: Operation canceled
using_future: Answer: using_future: Caught: Operation canceled
using_handler: Result: Success
using_handler: Answer: 42
using_handler: Result: Operation canceled
using_handler: Answer: 0

#MartiNitro's idea with packaged_task has become part of the library: now you can just post a packaged_task and it will magically return its future:
auto f = post(strand_, std::packaged_task<int()>(task));
Live Demo
#include <boost/asio.hpp>
#include <iostream>
#include <future>
using namespace std::chrono_literals;
int task() {
std::this_thread::sleep_for(1s);
std::cout << "Hello world\n";
return 42;
}
int main() {
boost::asio::thread_pool ioc;
auto strand_ = make_strand(ioc.get_executor());
auto f = post(strand_, std::packaged_task<int()>(task));
// optionally wait for future:
f.wait();
// otherwise .get() would block:
std::cout << "Answer: " << f.get() << "\n";
ioc.join();
}
Prints
Hello world
Answer: 42

Thas what I came up with, it essentially wrapts the asio::post and plugs in a promise/future pair. I think it can be adapted to your needs as well.
// outer scope setup
asio::io_context context;
asio::io_context::strand strand(context);
std::future<void> async_send(tcp::socket& socket, std::string message) {
auto buffered = std::make_shared<std::string>(message);
std::promise<void> promise;
auto future = promise.get_future();
// completion handler which only sets the promise.
auto handler = [buffered, promise{std::move(promise)}](asio::error_code, std::size_t) mutable {
promise.set_value();
};
// post async_write call to strand. Thas *should* protecte agains concurrent
// writes to the same socket from multiple threads
asio::post(strand, [buffered, &socket, handler{std::move(handler)}]() mutable {
asio::async_write(socket, asio::buffer(*buffered), asio::bind_executor(strand, std::move(handler)));
});
return future;
}
The promise can be moved without the future becoming invalidated.
Adapted to your scenario it could be somethign like this:
template<typename C>
std::future<void> post_with_future(C&& handler)
{
std::promise<void> promise;
auto future = promise.get_future();
auto wrapper = [promise{std::move(promise)}]{ // maybe mutable required?
handler();
promise.set_value();
};
// need to move in, cause the promise needs to be transferred. (i think)
asio::post(strand, std::move(wrapper));
return future;
}
I would be happy about some feedback to those lines, as I am myself just learning the whole thing :)
Hope to help,
Marti

Related

Do boost::asio c++20 coroutines support multithreading?

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:

c++20 coroutines: boost asio co_spawn blocks from another coroutine

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);

Best practise for boost::asio timeouts when using coroutines

The connection attempt below creates (on my network configuration) a delay of 2 minutes because the target does not exist and the address is on a different subnet to my machine. So I added timeout logic to limit the attempt to 5 seconds:
#define BOOST_ASIO_HAS_CO_AWAIT
#define BOOST_ASIO_HAS_STD_COROUTINE
#include <iostream>
#include <chrono>
#include <thread>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/connect.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/address_v4.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/executor_work_guard.hpp>
#include <boost/asio/redirect_error.hpp>
namespace this_coro = boost::asio::this_coro;
using namespace std::chrono_literals;
boost::asio::awaitable<void> fail_to_connect()
{
auto executor = co_await this_coro::executor;
auto socket = boost::asio::ip::tcp::socket{executor};
auto ep = boost::asio::ip::tcp::endpoint{
boost::asio::ip::make_address_v4("192.168.1.52"),
80};
auto timer = boost::asio::steady_timer{executor};
timer.expires_after(5s);
boost::asio::co_spawn(
executor,
[&]() -> boost::asio::awaitable<void> {
auto ec = boost::system::error_code{};
co_await timer.async_wait(boost::asio::redirect_error(boost::asio::use_awaitable, ec));
std::cout << "Thread ID: " << std::this_thread::get_id()
<< " Timer: " << ec.message() << std::endl;
if (!ec) {
socket.close();
}
},
boost::asio::detached
);
std::cout << "Thread ID: " << std::this_thread::get_id()
<< " Starting connection" << std::endl;
co_await boost::asio::async_connect(socket,
std::array{std::move(ep)},
boost::asio::use_awaitable);
timer.cancel();
}
int main()
{
auto ctx = boost::asio::io_context{};
auto guard = boost::asio::make_work_guard(ctx.get_executor());
auto exception_handler = [&](auto e_ptr) {
if (e_ptr) {
std::rethrow_exception(e_ptr);
}
};
boost::asio::co_spawn(ctx, fail_to_connect, std::move(exception_handler));
ctx.run();
}
This works as expected. As you can see from the thread ID's, using the same execution context between the two coroutines means that I'm not accessing the socket concurrently too.
14:58:41: Starting /home/cmannett85/workspace/build-scratch-Desktop-Debug/scratch ...
Thread ID: 140171855615808 Starting connection
Thread ID: 140171855615808 Timer: Success
terminate called after throwing an instance of 'boost::system::system_error'
what(): Operation canceled
14:58:46: The program has unexpectedly finished.
However this feels clunky, especially compared to the non-coroutine callback-style. Is there a better approach for timeouts using coroutines? I'm struggling to find examples.
One suggestion is to lift the boilerplate into a class.
Basically just a wrapper around a timer, that can set and cancel a callback with an easier interface:
#include "asio.hpp"
class timeout {
asio::high_resolution_timer timer_;
using duration = asio::high_resolution_timer::duration;
public:
template <typename Executor>
timeout(Executor&& executor) : timer_(executor){};
template <typename Executor, typename Func>
timeout(Executor&& executor, const duration& timeout, Func&& on_timeout)
: timer_(executor) {
set(timeout, on_timeout);
}
template <typename Func>
void set(const duration& timeout, Func&& on_timeout) {
timer_.expires_after(timeout); // cancels outstanding timeouts
timer_.async_wait([&](std::error_code ec) {
if (ec) return;
std::cout << "cancelling\n";
on_timeout();
});
}
void cancel() { timer_.cancel(); }
~timeout() { timer_.cancel(); }
};
This can be used like this:
tmo.set(1s, [&]() { socket.close(); });
std::size_t n = co_await asio::async_read_until(socket, incoming, '\n',
asio::use_awaitable);
asio::const_buffer response(asio::buffers_begin(incoming.data()),
asio::buffers_begin(incoming.data()) + n);
tmo.set(1s, [&]() { socket.close(); });
co_await asio::async_write(socket, response, asio::use_awaitable);
incoming.consume(n);
It can be generally used with any object that supports true cancellation, which is somewhat sparsely supported. For example, the only true way to cancel an ongoing read or write on a socket is to call close().
Cancelling an async_resolve for example, is not possible, because cancel() only cancels a pending operation, not an ongoing one. In other words, this doesn't work as expected:
// does not actually cancel anything :(
tmo.set(1s, [&]() { resolver.cancel(); });
resolver.async_resolve(...);
Here's a full example of an echo client that stops if any of these takes more than 1 second:
async_connect
async_read_until
async_write
As noted, async_resolve cannot be cancelled, and so writing a timeout for it doesn't make a lot of sense. Perhaps posting an exception to the coroutine's execution context would work instead?
#include <chrono>
#include <iostream>
#include <string>
#include <string_view>
// clang-format off
#ifdef _WIN32
#include "sdkddkver.h"
#endif
#include "asio.hpp"
// clang-format on
using asio::ip::tcp;
class timeout {
asio::high_resolution_timer timer_;
using duration = asio::high_resolution_timer::duration;
public:
template <typename Executor>
timeout(Executor&& executor) : timer_(executor){};
template <typename Executor, typename Func>
timeout(Executor&& executor, const duration& timeout, Func&& on_timeout)
: timer_(executor) {
set(timeout, on_timeout);
}
template <typename Func>
void set(const duration& timeout, Func&& on_timeout) {
timer_.expires_after(timeout); // cancels outstanding timeouts
timer_.async_wait([&](std::error_code ec) {
if (ec) return;
std::cout << "cancelling\n";
on_timeout();
});
}
void cancel() { timer_.cancel(); }
~timeout() { timer_.cancel(); }
};
asio::awaitable<void> echo_client(tcp::socket& socket, std::string_view host,
std::string_view service) {
try {
using namespace std::chrono_literals;
auto exec = socket.get_executor();
tcp::resolver resolver(exec);
timeout tmo(exec);
std::cout << "resolving...\n";
tmo.set(1s, [&]() { resolver.cancel(); });
auto endpoints =
co_await resolver.async_resolve(host, service, asio::use_awaitable);
std::cout << "connecting...\n";
tmo.set(1s, [&]() { socket.close(); });
co_await asio::async_connect(socket, endpoints, asio::use_awaitable);
asio::streambuf incoming;
for (;;) {
std::cout << "reading... ";
tmo.set(1s, [&]() { socket.close(); });
std::size_t n = co_await asio::async_read_until(socket, incoming, '\n',
asio::use_awaitable);
std::string response(asio::buffers_begin(incoming.data()),
asio::buffers_begin(incoming.data()) + n);
std::cout << response;
tmo.set(1s, [&]() { socket.close(); });
co_await asio::async_write(socket, asio::buffer(response),
asio::use_awaitable);
incoming.consume(n);
}
} catch (std::exception& e) {
std::cerr << e.what() << "\n";
}
}
int main(int argc, char** argv) {
if (argc < 3) {
std::cout << "usage: " << argv[0] << " [host] [port]\n";
return 1;
}
asio::io_context io_context;
tcp::socket socket(io_context);
asio::co_spawn(io_context, echo_client(socket, argv[1], argv[2]),
asio::detached);
io_context.run();
return 0;
}

Boost ASIO: Send message to all connected clients

I'm working on a project that involves a boost::beast websocket/http mixed server, which runs on top of boost::asio. I've heavily based my project off the advanced_server.cpp example source.
It works fine, but right now I'm attempting to add a feature that requires the sending of a message to all connected clients.
I'm not very familiar with boost::asio, but right now I can't see any way to have something like "broadcast" events (if that's even the correct term).
My naive approach would be to see if I can have the construction of websocket_session() attach something like an event listener, and the destructor detatch the listener. At that point, I could just fire the event, and have all the currently valid websocket sessions (to which the lifetime of websocket_session() is scoped) execute a callback.
There is https://stackoverflow.com/a/17029022/268006, which does more or less what I want by (ab)using a boost::asio::steady_timer, but that seems like a kind of horrible hack to accomplish something that should be pretty straightforward.
Basically, given a stateful boost::asio server, how can I do an operation on multiple connections?
First off: You can broadcast UDP, but that's not to connected clients. That's just... UDP.
Secondly, that link shows how to have a condition-variable (event)-like interface in Asio. That's only a tiny part of your problem. You forgot about the big picture: you need to know about the set of open connections, one way or the other:
e.g. keeping a container of session pointers (weak_ptr) to each connection
each connection subscribing to a signal slot (e.g. Boost Signals).
Option 1. is great for performance, option 2. is better for flexibility (decoupling the event source from subscribers, making it possible to have heterogenous subscribers, e.g. not from connections).
Because I think Option 1. is much simpler w.r.t to threading, better w.r.t. efficiency (you can e.g. serve all clients from one buffer without copying) and you probably don't need to doubly decouple the signal/slots, let me refer to an answer where I already showed as much for pure Asio (without Beast):
How to design proper release of a boost::asio socket or wrapper thereof
It shows the concept of a "connection pool" - which is essentially a thread-safe container of weak_ptr<connection> objects with some garbage collection logic.
Demonstration: Introducing Echo Server
After chatting about things I wanted to take the time to actually demonstrate the two approaches, so it's completely clear what I'm talking about.
First let's present a simple, run-of-the mill asynchronous TCP server with
with multiple concurrent connections
each connected session reads from the client line-by-line, and echoes the same back to the client
stops accepting after 3 seconds, and exits after the last client disconnects
master branch on github
#include <boost/asio.hpp>
#include <memory>
#include <list>
#include <iostream>
namespace ba = boost::asio;
using ba::ip::tcp;
using boost::system::error_code;
using namespace std::chrono_literals;
using namespace std::string_literals;
static bool s_verbose = false;
struct connection : std::enable_shared_from_this<connection> {
connection(ba::io_context& ioc) : _s(ioc) {}
void start() { read_loop(); }
void send(std::string msg, bool at_front = false) {
post(_s.get_io_service(), [=] { // _s.get_executor() for newest Asio
if (enqueue(std::move(msg), at_front))
write_loop();
});
}
private:
void do_echo() {
std::string line;
if (getline(std::istream(&_rx), line)) {
send(std::move(line) + '\n');
}
}
bool enqueue(std::string msg, bool at_front)
{ // returns true if need to start write loop
at_front &= !_tx.empty(); // no difference
if (at_front)
_tx.insert(std::next(begin(_tx)), std::move(msg));
else
_tx.push_back(std::move(msg));
return (_tx.size() == 1);
}
bool dequeue()
{ // returns true if more messages pending after dequeue
assert(!_tx.empty());
_tx.pop_front();
return !_tx.empty();
}
void write_loop() {
ba::async_write(_s, ba::buffer(_tx.front()), [this,self=shared_from_this()](error_code ec, size_t n) {
if (s_verbose) std::cout << "Tx: " << n << " bytes (" << ec.message() << ")" << std::endl;
if (!ec && dequeue()) write_loop();
});
}
void read_loop() {
ba::async_read_until(_s, _rx, "\n", [this,self=shared_from_this()](error_code ec, size_t n) {
if (s_verbose) std::cout << "Rx: " << n << " bytes (" << ec.message() << ")" << std::endl;
do_echo();
if (!ec)
read_loop();
});
}
friend struct server;
ba::streambuf _rx;
std::list<std::string> _tx;
tcp::socket _s;
};
struct server {
server(ba::io_context& ioc) : _ioc(ioc) {
_acc.bind({{}, 6767});
_acc.set_option(tcp::acceptor::reuse_address());
_acc.listen();
accept_loop();
}
void stop() {
_ioc.post([=] {
_acc.cancel();
_acc.close();
});
}
private:
void accept_loop() {
auto session = std::make_shared<connection>(_acc.get_io_context());
_acc.async_accept(session->_s, [this,session](error_code ec) {
auto ep = ec? tcp::endpoint{} : session->_s.remote_endpoint();
std::cout << "Accept from " << ep << " (" << ec.message() << ")" << std::endl;
session->start();
if (!ec)
accept_loop();
});
}
ba::io_context& _ioc;
tcp::acceptor _acc{_ioc, tcp::v4()};
};
int main(int argc, char** argv) {
s_verbose = argc>1 && argv[1] == "-v"s;
ba::io_context ioc;
server s(ioc);
std::thread th([&ioc] { ioc.run(); }); // todo exception handling
std::this_thread::sleep_for(3s);
s.stop(); // active connections will continue
th.join();
}
Approach 1. Adding Broadcast Messages
So, let's add "broadcast messages" that get sent to all active connections simultaneously. We add two:
one at each new connection (saying "Player ## has entered the game")
one that emulates a global "server event", like you described in the question). It gets triggered from within main:
std::this_thread::sleep_for(1s);
auto n = s.broadcast("random global event broadcast\n");
std::cout << "Global event broadcast reached " << n << " active connections\n";
Note how we do this by registering a weak pointer to each accepted connection and operating on each:
_acc.async_accept(session->_s, [this,session](error_code ec) {
auto ep = ec? tcp::endpoint{} : session->_s.remote_endpoint();
std::cout << "Accept from " << ep << " (" << ec.message() << ")" << std::endl;
if (!ec) {
auto n = reg_connection(session);
session->start();
accept_loop();
broadcast("player #" + std::to_string(n) + " has entered the game\n");
}
});
broadcast is also used directly from main and is simply:
size_t broadcast(std::string const& msg) {
return for_each_active([msg](connection& c) { c.send(msg, true); });
}
using-asio-post branch on github
#include <boost/asio.hpp>
#include <memory>
#include <list>
#include <iostream>
namespace ba = boost::asio;
using ba::ip::tcp;
using boost::system::error_code;
using namespace std::chrono_literals;
using namespace std::string_literals;
static bool s_verbose = false;
struct connection : std::enable_shared_from_this<connection> {
connection(ba::io_context& ioc) : _s(ioc) {}
void start() { read_loop(); }
void send(std::string msg, bool at_front = false) {
post(_s.get_io_service(), [=] { // _s.get_executor() for newest Asio
if (enqueue(std::move(msg), at_front))
write_loop();
});
}
private:
void do_echo() {
std::string line;
if (getline(std::istream(&_rx), line)) {
send(std::move(line) + '\n');
}
}
bool enqueue(std::string msg, bool at_front)
{ // returns true if need to start write loop
at_front &= !_tx.empty(); // no difference
if (at_front)
_tx.insert(std::next(begin(_tx)), std::move(msg));
else
_tx.push_back(std::move(msg));
return (_tx.size() == 1);
}
bool dequeue()
{ // returns true if more messages pending after dequeue
assert(!_tx.empty());
_tx.pop_front();
return !_tx.empty();
}
void write_loop() {
ba::async_write(_s, ba::buffer(_tx.front()), [this,self=shared_from_this()](error_code ec, size_t n) {
if (s_verbose) std::cout << "Tx: " << n << " bytes (" << ec.message() << ")" << std::endl;
if (!ec && dequeue()) write_loop();
});
}
void read_loop() {
ba::async_read_until(_s, _rx, "\n", [this,self=shared_from_this()](error_code ec, size_t n) {
if (s_verbose) std::cout << "Rx: " << n << " bytes (" << ec.message() << ")" << std::endl;
do_echo();
if (!ec)
read_loop();
});
}
friend struct server;
ba::streambuf _rx;
std::list<std::string> _tx;
tcp::socket _s;
};
struct server {
server(ba::io_context& ioc) : _ioc(ioc) {
_acc.bind({{}, 6767});
_acc.set_option(tcp::acceptor::reuse_address());
_acc.listen();
accept_loop();
}
void stop() {
_ioc.post([=] {
_acc.cancel();
_acc.close();
});
}
size_t broadcast(std::string const& msg) {
return for_each_active([msg](connection& c) { c.send(msg, true); });
}
private:
using connptr = std::shared_ptr<connection>;
using weakptr = std::weak_ptr<connection>;
std::mutex _mx;
std::vector<weakptr> _registered;
size_t reg_connection(weakptr wp) {
std::lock_guard<std::mutex> lk(_mx);
_registered.push_back(wp);
return _registered.size();
}
template <typename F>
size_t for_each_active(F f) {
std::vector<connptr> active;
{
std::lock_guard<std::mutex> lk(_mx);
for (auto& w : _registered)
if (auto c = w.lock())
active.push_back(c);
}
for (auto& c : active) {
std::cout << "(running action for " << c->_s.remote_endpoint() << ")" << std::endl;
f(*c);
}
return active.size();
}
void accept_loop() {
auto session = std::make_shared<connection>(_acc.get_io_context());
_acc.async_accept(session->_s, [this,session](error_code ec) {
auto ep = ec? tcp::endpoint{} : session->_s.remote_endpoint();
std::cout << "Accept from " << ep << " (" << ec.message() << ")" << std::endl;
if (!ec) {
auto n = reg_connection(session);
session->start();
accept_loop();
broadcast("player #" + std::to_string(n) + " has entered the game\n");
}
});
}
ba::io_context& _ioc;
tcp::acceptor _acc{_ioc, tcp::v4()};
};
int main(int argc, char** argv) {
s_verbose = argc>1 && argv[1] == "-v"s;
ba::io_context ioc;
server s(ioc);
std::thread th([&ioc] { ioc.run(); }); // todo exception handling
std::this_thread::sleep_for(1s);
auto n = s.broadcast("random global event broadcast\n");
std::cout << "Global event broadcast reached " << n << " active connections\n";
std::this_thread::sleep_for(2s);
s.stop(); // active connections will continue
th.join();
}
Approach 2: Those Broadcast But With Boost Signals2
The Signals approach is a fine example of Dependency Inversion.
Most salient notes:
signal slots get invoked on the thread invoking it ("raising the event")
the scoped_connection is there so subscriptions are *automatically removed when the connection is destructed
there's subtle difference in the wording of the console message from "reached # active connections" to "reached # active subscribers".
The difference is key to understanding the added flexibility: the signal owner/invoker does not know anything about the subscribers. That's the decoupling/dependency inversion we're talking about
using-signals2 branch on github
#include <boost/asio.hpp>
#include <memory>
#include <list>
#include <iostream>
#include <boost/signals2.hpp>
namespace ba = boost::asio;
using ba::ip::tcp;
using boost::system::error_code;
using namespace std::chrono_literals;
using namespace std::string_literals;
static bool s_verbose = false;
struct connection : std::enable_shared_from_this<connection> {
connection(ba::io_context& ioc) : _s(ioc) {}
void start() { read_loop(); }
void send(std::string msg, bool at_front = false) {
post(_s.get_io_service(), [=] { // _s.get_executor() for newest Asio
if (enqueue(std::move(msg), at_front))
write_loop();
});
}
private:
void do_echo() {
std::string line;
if (getline(std::istream(&_rx), line)) {
send(std::move(line) + '\n');
}
}
bool enqueue(std::string msg, bool at_front)
{ // returns true if need to start write loop
at_front &= !_tx.empty(); // no difference
if (at_front)
_tx.insert(std::next(begin(_tx)), std::move(msg));
else
_tx.push_back(std::move(msg));
return (_tx.size() == 1);
}
bool dequeue()
{ // returns true if more messages pending after dequeue
assert(!_tx.empty());
_tx.pop_front();
return !_tx.empty();
}
void write_loop() {
ba::async_write(_s, ba::buffer(_tx.front()), [this,self=shared_from_this()](error_code ec, size_t n) {
if (s_verbose) std::cout << "Tx: " << n << " bytes (" << ec.message() << ")" << std::endl;
if (!ec && dequeue()) write_loop();
});
}
void read_loop() {
ba::async_read_until(_s, _rx, "\n", [this,self=shared_from_this()](error_code ec, size_t n) {
if (s_verbose) std::cout << "Rx: " << n << " bytes (" << ec.message() << ")" << std::endl;
do_echo();
if (!ec)
read_loop();
});
}
friend struct server;
ba::streambuf _rx;
std::list<std::string> _tx;
tcp::socket _s;
boost::signals2::scoped_connection _subscription;
};
struct server {
server(ba::io_context& ioc) : _ioc(ioc) {
_acc.bind({{}, 6767});
_acc.set_option(tcp::acceptor::reuse_address());
_acc.listen();
accept_loop();
}
void stop() {
_ioc.post([=] {
_acc.cancel();
_acc.close();
});
}
size_t broadcast(std::string const& msg) {
_broadcast_event(msg);
return _broadcast_event.num_slots();
}
private:
boost::signals2::signal<void(std::string const& msg)> _broadcast_event;
size_t reg_connection(connection& c) {
c._subscription = _broadcast_event.connect(
[&c](std::string msg){ c.send(msg, true); }
);
return _broadcast_event.num_slots();
}
void accept_loop() {
auto session = std::make_shared<connection>(_acc.get_io_context());
_acc.async_accept(session->_s, [this,session](error_code ec) {
auto ep = ec? tcp::endpoint{} : session->_s.remote_endpoint();
std::cout << "Accept from " << ep << " (" << ec.message() << ")" << std::endl;
if (!ec) {
auto n = reg_connection(*session);
session->start();
accept_loop();
broadcast("player #" + std::to_string(n) + " has entered the game\n");
}
});
}
ba::io_context& _ioc;
tcp::acceptor _acc{_ioc, tcp::v4()};
};
int main(int argc, char** argv) {
s_verbose = argc>1 && argv[1] == "-v"s;
ba::io_context ioc;
server s(ioc);
std::thread th([&ioc] { ioc.run(); }); // todo exception handling
std::this_thread::sleep_for(1s);
auto n = s.broadcast("random global event broadcast\n");
std::cout << "Global event broadcast reached " << n << " active subscribers\n";
std::this_thread::sleep_for(2s);
s.stop(); // active connections will continue
th.join();
}
See the diff between Approach 1. and 2.: Compare View on github
A sample of the output when run against 3 concurrent clients with:
(for a in {1..3}; do netcat localhost 6767 < /etc/dictionaries-common/words > echoed.$a& sleep .1; done; time wait)
The answer from #sehe was amazing, so I'll be brief. Generally speaking, to implement an algorithm which operates on all active connections you must do the following:
Maintain a list of active connections. If this list is accessed by multiple threads, it will need synchronization (std::mutex). New connections should be inserted to the list, and when a connection is destroyed or becomes inactive it should be removed from the list.
To iterate the list, synchronization is required if the list is accessed by multiple threads (i.e. more than one thread calling asio::io_context::run, or if the list is also accessed from threads that are not calling asio::io_context::run)
During iteration, if the algorithm needs to inspect or modify the state of any connection, and that state can be changed by other threads, additional synchronization is needed. This includes any internal "queue" of messages that the connection object stores.
A simple way to synchronize a connection object is to use boost::asio::post to submit a function for execution on the connection object's context, which will be either an explicit strand (boost::asio::strand, as in the advanced server examples) or an implicit strand (what you get when only one thread calls io_context::run). The Approach 1 provided by #sehe uses post to synchronize in this fashion.
Another way to synchronize the connection object is to "stop the world." That means call io_context::stop, wait for all the threads to exit, and then you are guaranteed that no other threads are accessing the list of connections. Then you can read and write connection object state all you want. When you are finished with the list of connections, call io_context::restart and launch the threads which call io_context::run again. Stopping the io_context does not stop network activity, the kernel and network drivers still send and receive data from internal buffers. TCP/IP flow control will take care of things so the application still operates smoothly even though it becomes briefly unresponsive during the "stop the world." This approach can simplify things but depending on your particular application you will have to evaluate if it is right for you.
Hope this helps!
Thank you #sehe for the amazing answer. Still, I think there is a small but severe bug in the Approach 2. IMHO reg_connection should look like this:
size_t reg_connection(std::shared_ptr<connection> c) {
c->_subscription = _broadcast_event.connect(
[weak_c = std::weak_ptr<connection>(c)](std::string msg){
if(auto c = weak_c.lock())
c->send(msg, true);
}
);
return _broadcast_event.num_slots();
}
Otherwise you can end up with a race condition leading to a server crash. In case the connection instance is destroyed during the call to the lambda, the reference becomes invalid.
Similarly connection#send() should look like this, because otherwise this might be dead by the time the lambda is called:
void send(std::string msg, bool at_front = false) {
post(_s.get_io_service(),
[self=shared_from_this(), msg=std::move(msg), at_front] {
if (self->enqueue(std::move(msg), at_front))
self->write_loop();
});
}
PS: I would have posted this as a comment on #sehe's answer, but unfortunately I have not enough reputation.

How to set error_code to asio::yield_context

I'd like to create an asynchronous function which takes as it's last argument boost::asio::yield_context. E.g.:
int async_meaning_of_life(asio::yield_context yield);
I'd also like to be consistent with how Asio returns error codes. That is, if the user does:
int result = async_meaning_of_life(yield);
and the function fails, then it throws the system_error exception. But if the user does:
boost::error_code ec;
int result = async_meaning_of_life(yield[ec]);
Then - instead of throwing - the error is returned in ec.
The problem is that when implementing the function, I can't seem to find a clean way to check whether the operator[] was used or not and set it if so. We came up with something like this:
inline void set_error(asio::yield_context yield, sys::error_code ec)
{
if (!yield.ec_) throw system_error(ec);
*(yield.ec_) = ec;
}
But that's hacky, because yield_context::ec_ is declared private (although only in the documentation).
One other way I can think of doing this is to convert the yield object into asio::handler_type and execute it. But this solution seems awkward at best.
Is there another way?
Asio uses async_result to transparently provide use_future, yield_context or completion handlers in its API interfaces.¹
Here's how the pattern goes:
template <typename Token>
auto async_meaning_of_life(bool success, Token&& token)
{
typename asio::handler_type<Token, void(error_code, int)>::type
handler (std::forward<Token> (token));
asio::async_result<decltype (handler)> result (handler);
if (success)
handler(42);
else
handler(asio::error::operation_aborted, 0);
return result.get ();
}
Update
Starting with Boost 1.66 the pattern adheres to the interface proposed for standardization:
using result_type = typename asio::async_result<std::decay_t<Token>, void(error_code, int)>;
typename result_type::completion_handler_type handler(std::forward<Token>(token));
result_type result(handler);
Comprehensive Demo
Showing how to use it with with
coro's and yield[ec]
coro's and yield + exceptions
std::future
completion handlers
Live On Coliru
#define BOOST_COROUTINES_NO_DEPRECATION_WARNING
#include <iostream>
#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/use_future.hpp>
using boost::system::error_code;
namespace asio = boost::asio;
template <typename Token>
auto async_meaning_of_life(bool success, Token&& token)
{
#if BOOST_VERSION >= 106600
using result_type = typename asio::async_result<std::decay_t<Token>, void(error_code, int)>;
typename result_type::completion_handler_type handler(std::forward<Token>(token));
result_type result(handler);
#else
typename asio::handler_type<Token, void(error_code, int)>::type
handler(std::forward<Token>(token));
asio::async_result<decltype (handler)> result (handler);
#endif
if (success)
handler(error_code{}, 42);
else
handler(asio::error::operation_aborted, 0);
return result.get ();
}
void using_yield_ec(asio::yield_context yield) {
for (bool success : { true, false }) {
boost::system::error_code ec;
auto answer = async_meaning_of_life(success, yield[ec]);
std::cout << __FUNCTION__ << ": Result: " << ec.message() << "\n";
std::cout << __FUNCTION__ << ": Answer: " << answer << "\n";
}
}
void using_yield_catch(asio::yield_context yield) {
for (bool success : { true, false })
try {
auto answer = async_meaning_of_life(success, yield);
std::cout << __FUNCTION__ << ": Answer: " << answer << "\n";
} catch(boost::system::system_error const& e) {
std::cout << __FUNCTION__ << ": Caught: " << e.code().message() << "\n";
}
}
void using_future() {
for (bool success : { true, false })
try {
auto answer = async_meaning_of_life(success, asio::use_future);
std::cout << __FUNCTION__ << ": Answer: " << answer.get() << "\n";
} catch(boost::system::system_error const& e) {
std::cout << __FUNCTION__ << ": Caught: " << e.code().message() << "\n";
}
}
void using_handler() {
for (bool success : { true, false })
async_meaning_of_life(success, [](error_code ec, int answer) {
std::cout << "using_handler: Result: " << ec.message() << "\n";
std::cout << "using_handler: Answer: " << answer << "\n";
});
}
int main() {
asio::io_service svc;
spawn(svc, using_yield_ec);
spawn(svc, using_yield_catch);
std::thread work([] {
using_future();
using_handler();
});
svc.run();
work.join();
}
Prints:
using_yield_ec: Result: Success
using_yield_ec: Answer: 42
using_yield_ec: Result: Operation canceled
using_yield_ec: Answer: 0
using_future: Answer: 42
using_yield_catch: Answer: 42
using_yield_catch: Caught: Operation canceled
using_future: Caught: Operation canceled
using_handler: Result: Success
using_handler: Answer: 42
using_handler: Result: Operation canceled
using_handler: Answer: 0
Note: for simplicity I have not added output synchronization, so the output can become intermingled depending on runtime execution order
¹ see e.g. this excellent demonstration of how to use it to extend the library with your own async result pattern boost::asio with boost::unique_future