In the 5th tutorial, of which the code I have given at bottom of the question, asio documentation introduced the output comes as follows :
Timer 2: 0
Timer 1: 1
Timer 2: 2
Timer 1: 3
Timer 2: 4
.
.
.
After the first one it is as expectable, with the sequence.
But even though Timer1 is wrapped in the strand first, why does Timer 2 starts running first ?
#include <iostream>
#include <asio.hpp>
#include <boost/bind.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>
class printer
{
public:
printer(asio::io_service& io)
: strand_(io),
timer1_(io, boost::posix_time::seconds(1)),
timer2_(io, boost::posix_time::seconds(1)),
count_(0)
{
timer1_.async_wait(strand_.wrap(boost::bind(&printer::print1, this)));
timer2_.async_wait(strand_.wrap(boost::bind(&printer::print2, this)));
}
~printer()
{
std::cout << "Final count is " << count_ << "\n";
}
void print1()
{
if (count_ < 10)
{
std::cout << "Timer 1: " << count_ << "\n";
++count_;
timer1_.expires_at(timer1_.expires_at() + boost::posix_time::seconds(1));
timer1_.async_wait(strand_.wrap(boost::bind(&printer::print1, this)));
}
}
void print2()
{
if (count_ < 10)
{
std::cout << "Timer 2: " << count_ << "\n";
++count_;
timer2_.expires_at(timer2_.expires_at() + boost::posix_time::seconds(1));
timer2_.async_wait(strand_.wrap(boost::bind(&printer::print2, this)));
}
}
private:
asio::strand strand_;
asio::deadline_timer timer1_;
asio::deadline_timer timer2_;
int count_;
};
int main()
{
asio::io_service io;
printer p(io);
asio::thread t(boost::bind(&asio::io_service::run, &io));
io.run();
t.join();
system("PAUSE");
return 0;
}
A strand is used to provide serial execution of handlers. Also, under certain conditions, it provides a guarantee on the order of invocation of handlers posted or dispatched through the strand. The example does not meet these conditions. Furthermore, there is no guarantee that one will observe the alternating pattern between the completion handlers.
IO Objects, such as timers, are not wrapped by strands, completion handlers are. A strand can be thought of as being associated with a FIFO queue of handlers. If a handler queue has no handlers currently posted into an io_service, then it will pop one handler from itself and post it into the associated io_service. This flow guarantees that handlers posted into the same strand will not be invoked concurrently.
strand.post() enqueues a handler into the strand.
strand.dispatch() will run the handler if the current caller is running within the context of the strand. Otherwise, it will enqueue the handler as if by post().
strand.wrap() return a new completion handler that, when invoked, will dispatch() the wrapped handler into the strand. Essentially, wrap() defers the dispatching of a handler into the strand.
Given completion handlers a and b, if a is enqueued before b, then a will be invoked before b. This is the fundamental guarantee to which all scenarios can be reduced. The scenarios in which a is guaranteed before b are documented as followed:
strand.post(a) happens before strand.post(b). As post() does not attempt to invoke the provided handler within post(), a is enqueued before b.
strand.post(a) happens before strand.dispatch(b), where strand.dispatch(b) is performed outside of a strand. As strand.dispatch(b) occurs outside of a strand, b is queued as if by post(). Thus, this reduces down to strand.post(a) happening before strand.post(b).
strand.dispatch(a) happens before strand.post(b), where strand.dispatch(a) occurs outside of a strand. As strand.dispatch(a) occurs outside of a strand, a is queued as if by post(). Thus, this reduces down to strand.post(a) happening before strand.post(b).
strand.dispatch(a) happens before strand.dispatch(b), where both are performed outside of the strand. As neither occur within a strand, both handlers are enqueued as if by post(). Thus, this reduces down to strand.post(a) happening before strand.post(b).
The io_service makes no guarantees about the invocation order of handlers. Additionally, the handler returned from strand.wrap() does not run within the context of a strand. The example code simplifies to:
auto wrapped_print1 = strand.wrap(&print1);
auto wrapped_print2 = strand.wrap(&print2);
timer1_.async_wait(wrapped_print1);
timer2_.async_wait(wrapped_print2);
If the async_wait operations complete at the same time, the wrapped_print1 and wrapped_print2 completion handlers will be posted into the io_service for deferred invocation. As the io_service makes no guarantees on the invocation order, it may choose to invoke wrapped_print1 first, or it may choose to invoke wrapped_print2 first. Both wrapped_print handlers are being invoked outside of the context of the strand in an unspecified order, resulting in print1() and print2() being enqueued into the strand in an unspecified order.
The unspecified order in which wrapped_print are invoked is why one is not guaranteed to observe an alternating pattern between the print1 and print2 handlers in the original example. However, given the current implementation of the io_service's internal scheduler, one will observe such a pattern.
Related
Following Michael Caisse's cppcon talk I created a connection handler MyUserConnection which has a sendMessage method. sendMessage method adds a message to the queue similarly to the send() in the cppcon talk. My sendMessage method is called from multiple threads outside of the connection handler in high intervals. The messages must be enqueued chronologically.
When I run my code with only one Asio io_service::run call (aka one io_service thread) it async_write's and empties my queue as expected (FIFO), however, the problem occurs when there are, for example, 4 io_service::run calls, then the queue is not filled or the send calls are not called chronologically.
class MyUserConnection : public std::enable_shared_from_this<MyUserConnection> {
public:
MyUserConnection(asio::io_service& io_service, SslSocket socket) :
service_(io_service),
socket_(std::move(socket)),
strand_(io_service) {
}
void sendMessage(std::string msg) {
auto self(shared_from_this());
service_.post(strand_.wrap([self, msg]() {
self->queueMessage(msg);
}));
}
private:
void queueMessage(const std::string& msg) {
bool writeInProgress = !sendPacketQueue_.empty();
sendPacketQueue_.push_back(msg);
if (!writeInProgress) {
startPacketSend();
}
}
void startPacketSend() {
auto self(shared_from_this());
asio::async_write(socket_,
asio::buffer(sendPacketQueue_.front().data(), sendPacketQueue_.front().length()),
strand_.wrap([self](const std::error_code& ec, std::size_t /*n*/) {
self->packetSendDone(ec);
}));
}
void packetSendDone(const std::error_code& ec) {
if (!ec) {
sendPacketQueue_.pop_front();
if (!sendPacketQueue_.empty()) { startPacketSend(); }
} else {
// end(); // My end call
}
}
asio::io_service& service_;
SslSocket socket_;
asio::io_service::strand strand_;
std::deque<std::string> sendPacketQueue_;
};
I'm quite sure that I misinterpreted the strand and io_service::post when running the connection handler on multithreaded io_service. I'm also quite sure that the messages are not enqueued chronologically instead of messages not being async_write chronologically. How to ensure that the messages will be enqueued in chronological order in sendMessage call on multithreaded io_service?
If you use a strand, the order is guaranteed to be the order in which you post the operations to the strand.
Of course, if there is some kind of "correct ordering" between threads that post then you have to synchronize the posting between them, that's your application domain.
Here's a modernized, simplified take on your MyUserConnection class with a self-contained server test program:
Live On Coliru
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <deque>
#include <iostream>
#include <mutex>
namespace asio = boost::asio;
namespace ssl = asio::ssl;
using asio::ip::tcp;
using boost::system::error_code;
using SslSocket = ssl::stream<tcp::socket>;
class MyUserConnection : public std::enable_shared_from_this<MyUserConnection> {
public:
MyUserConnection(SslSocket&& socket) : socket_(std::move(socket)) {}
void start() {
std::cerr << "Handshake initiated" << std::endl;
socket_.async_handshake(ssl::stream_base::handshake_type::server,
[self = shared_from_this()](error_code ec) {
std::cerr << "Handshake complete" << std::endl;
});
}
void sendMessage(std::string msg) {
post(socket_.get_executor(),
[self = shared_from_this(), msg = std::move(msg)]() {
self->queueMessage(msg);
});
}
private:
void queueMessage(std::string msg) {
outbox_.push_back(std::move(msg));
if (outbox_.size() == 1)
sendLoop();
}
void sendLoop() {
std::cerr << "Sendloop " << outbox_.size() << std::endl;
if (outbox_.empty())
return;
asio::async_write( //
socket_, asio::buffer(outbox_.front()),
[this, self = shared_from_this()](error_code ec, std::size_t) {
if (!ec) {
outbox_.pop_front();
sendLoop();
} else {
end();
}
});
}
void end() {}
SslSocket socket_;
std::deque<std::string> outbox_;
};
int main() {
asio::thread_pool ioc;
ssl::context ctx(ssl::context::sslv23_server);
ctx.set_password_callback([](auto...) { return "test"; });
ctx.use_certificate_file("server.pem", ssl::context::file_format::pem);
ctx.use_private_key_file("server.pem", ssl::context::file_format::pem);
ctx.use_tmp_dh_file("dh2048.pem");
tcp::acceptor a(ioc, {{}, 8989u});
for (;;) {
auto s = a.accept(make_strand(ioc.get_executor()));
std::cerr << "accepted " << s.remote_endpoint() << std::endl;
auto sess = make_shared<MyUserConnection>(SslSocket(std::move(s), ctx));
sess->start();
for(int i = 0; i<30; ++i) {
post(ioc, [sess, i] {
std::string msg = "message #" + std::to_string(i) + "\n";
{
static std::mutex mx;
// Lock so console output is guaranteed in the same order
// as the sendMessage call
std::lock_guard lk(mx);
std::cout << "Sending " << msg << std::flush;
sess->sendMessage(std::move(msg));
}
});
}
break; // for online demo
}
ioc.join();
}
If you run it a few times, you will see that
the order in which the threads post is not deterministic (that's up to the kernel scheduling)
the order in which messages are sent (and received) is exactly the order in which they are posted.
See live demo runs on my machine:
On a multi core or even on a single core preemptive OS, you cannot truly feed messages into a queue in strictly chronological order. Even if you use a mutex to synchronize write access to the queue, the strict order is no longer guaranteed once multiple writers wait on the mutex and the mutex becomes free. At best, the order, in which the waiting write threads acquire the mutex, is implementation dependent (OS code dependent), but it is best to assume it is just random.
With that being said, the strict chronological order is a matter of definition in the first place. To explain that, imagine your PC has some digital output bits (1 for each writer thread) and you connected a logic analyzer to those bits.... and imagine, you pick some spot in the code, where you toggle such a respective bit in your enqueue function. Even if that bit toggle takes place just one assembly instruction prior to acquiring the mutex, it is possible, that the order had been changed, while the writer code approached that point. You could also set it to other arbirtrary points prior (e.g. when you enter the enqueue function). But then, the same reasoning applies. Hence, the strict chronological order is in itself a matter of definition.
There is an analogy to a case, where a CPUs interrupt controller has multiple inputs and you tried to build a system which processes those interrupts in strictly chronological order. Even if all interrupt inputs were signaled exactly at the same moment (a switch, pulling them all to signaled state simultaneously), some order would occur (e.g. caused by hardware logic or just by noise at the input pins or by the systems interrupt dispatcher function (some CPUs (e.g. MIPS 4102) have a single interrupt vector and assembly code checks the possible interrupt sources and dispatches to dedicated interrupt handlers).
This analogy helps see the pattern: It comes down to asynchronous inputs on a synchronous system. Which is a notoriously hard problem in itself.
So, the best you could possibly do, is to make a suitable definition of your applications "strict ordering" and live with it.
Then, to avoid violations of your definition, you could use a priority queue instead of a normal FIFO data type and use as priority some atomic counter:
At your chosen point in the code, atomically read and increment the counter.
This is your messages sequence number.
Assemble your message and enqueue it into the priority queue, using your sequence number as priority.
Another possible approach is to define a notion of "simultaneous", which is detectable on the other side of the queue (and thus, the reader cannot assume strict ordering for a set of "simultaneous" messages). This could be implemented by reading some high frequency tick count and all those messages, which have the same "time stamp" are to be considered simultaneous on reader side.
Regarding this post:
Why do I need strand per connection when using boost::asio?
I'm focusing on this statement regarding async calls:
"However, it is not safe for multiple threads to make calls concurrently"
This example:
http://www.boost.org/doc/libs/1_55_0/doc/html/boost_asio/example/cpp11/chat/chat_client.cpp
If I refer to main as "thread 1" and the spawned thread t as "thread 2", then it seems like thread 1 is calling async_write (assuming no write_in_progress) while thread 2 is calling async_read. What am I missing?
In the official chat example, chat_client::write() defers work to the io_service via io_service::post(), which will:
request that the io_service execute the given handler via a thread that is currently invoking the poll(), poll_one(), run(), or run_one() function on the io_service
not allow the given handler to be invoked within the calling function (e.g. chat_client::write())
As only one thread is running the io_service, and all socket read, write, and close operations are only initiated from handlers that have been posted to the io_service, the program satisfies the thread-safety requirement for socket.
class chat_client
{
void write(const chat_message& msg)
{
// The nullary function `handler` is created, but not invoked within
// the calling function. `msg` is captured by value, allowing `handler`
// to append a valid `msg` object to `write_msgs_`.
auto handler = [this, msg]()
{
bool write_in_progress = !write_msgs_.empty();
write_msgs_.push_back(msg);
if (!write_in_progress)
{
do_write();
}
};
// Request that `handler` be invoked within the `io_service`.
io_service_.post(handler);
}
};
Is calling asio::io_service::poll() or poll_one() in a nested or recursive fashion (ie. from within a handler) valid?
A really basic test seems to imply that this works (I've only done the test on one platform) but I want to be sure that calling poll() again from within a handler is considered valid behavior.
I couldn't find any relevant information in the asio docs, so I'm hoping that someone with a bit more experience with asio's inner workings could verify this with an explanation or references.
Basic test:
struct NestedHandler
{
NestedHandler(std::string name, asio::io_service * service) :
name(name),
service(service)
{
// empty
}
void operator()()
{
std::cout << " { ";
std::cout << name;
std::cout << " ...calling poll again... ";
service->poll();
std::cout << " } ";
}
std::string name;
asio::io_service * service;
};
struct DefaultHandler
{
DefaultHandler(std::string name) :
name(name)
{
// empty
}
void operator()()
{
std::cout << " { ";
std::cout << name;
std::cout << " } ";
}
std::string name;
};
int main()
{
asio::io_service service;
service.post(NestedHandler("N",&service));
service.post(DefaultHandler("A"));
service.post(DefaultHandler("B"));
service.post(DefaultHandler("C"));
service.post(DefaultHandler("D"));
std::cout << "asio poll" << std::endl;
service.poll();
return 0;
}
// Output:
asio poll
{ N ...calling poll again... { A } { B } { C } { D } }
It is valid.
For the family of functions that process the io_service, run() is the only one with restrictions:
The run() function must not be called from a thread that is currently calling one of run(), run_one(), poll() or poll_one() on the same io_service object.
However, I am inclined to think that the documentation should also include the same remark for run_one(), as a nested call can result in it blocking indefinitely for either of the following cases[1]:
the only work in the io_service is the handler currently being executed
for non I/O completion port implementations, the only work was posted from within the current handler and the io_service has a concurrency hint of 1
For Windows I/O completion ports, demultiplexing is performed in all threads servicing the io_service using GetQueuedCompletionStatus(). At a high-level, threads calling GetQueuedCompletionStatus() function as if they are part of a thread pool, allowing the OS to dispatch work to each thread. As no single thread is responsible for demultiplexing operations to other threads, nested calls to poll() or poll_one() do not affect operation dispatching for other threads. The documentation states:
Demultiplexing using I/O completion ports is performed in all threads that call io_service::run(), io_service::run_one(), io_service::poll() or io_service::poll_one().
For all other demultiplexing mechanisms systems, a single thread servicing io_service is used to demultiplex I/O operations. The exact demultiplexing mechanism can be found in the Platform-Specific Implementation Notes:
Demultiplexing using [/dev/poll, epoll, kqueue, select] is performed in one of the threads that calls io_service::run(), io_service::run_one(), io_service::poll() or io_service::poll_one().
The implementation for the demultiplexing mechanism differs slightly, but at a high-level:
the io_service has a main queue from which threads consume ready-to-run operations to perform
each call to process the io_service creates a private queue on the stack that is used to manage operations in a lock-free manner
synchronization with the main queue eventually occurs, where a lock is acquired and the private queue operations are copied into the main queue, informing other threads, and allowing them to consume from the main queue.
When the io_service is constructed, it may be provided a concurrency hint, suggesting how many threads the implementation should allow to run concurrently. When non-I/O completion port implementations are provided a concurrency hint of 1, they are optimized to use the private queue as much as possible and defer synchronization with the main queue. For example, when a handler is posted via post():
if invoked from outside of a handler, then the io_service guarantees thread safety so it locks the main queue before enqueueing the handler.
if invoked from within a handler, the posted handler is enqueued into the private queue, deferring deferring synchronization with the main queue until necessary.
When a nested poll() or poll_one() is invoked, it becomes necessary for the private queue to be copied into the main queue, as operations to be performed will be consumed from the main queue. This case is explicitly checked within the implementation:
// We want to support nested calls to poll() and poll_one(), so any handlers
// that are already on a thread-private queue need to be put on to the main
// queue now.
if (one_thread_)
if (thread_info* outer_thread_info = ctx.next_by_key())
op_queue_.push(outer_thread_info->private_op_queue);
When either no concurrency hint or any value other than 1 is provided, then posted handlers are synchronized into the main queue each time. As the private queue does not need to be copied, nested poll() and poll_one() calls will function as normal.
1. In the networking-ts draft, it is noted that run_one() must not be called from a thread that is currently calling run().
Is calling asio::io_service::poll() or poll_one() in a nested or
recursive fashion (ie. from within a handler) valid?
Sytaxically, this is valid. But, its not good bacause in every handler you should run poll(). Also, your stack trace will grow to very big sizes, and you can get big problems with the stack.
using Yield = asio::yield_context;
using boost::system::error_code;
int Func(Yield yield) {
error_code ec;
asio::detail::async_result_init<Yield, void(error_code, int)> init(yield[ec]);
std::thread th(std::bind(Process, init.handler));
int result = init.result.get(); // <--- yield at here
return result;
}
How to implement Process so that Func will resumed in the context of the strand that Func was originally spawned on?
Boost.Asio uses a helper function, asio_handler_invoke, to provide a customization point for invocation strategies. For example, when a Handler has been wrapped by a strand, the invocation strategy will cause the handler to be dispatched through the strand upon invocation. As noted in the documentation, asio_handler_invoke should be invoked via argument-dependent lookup.
using boost::asio::asio_handler_invoke;
asio_handler_invoke(nullary_functor, &handler);
For stackful coroutines, there are various important details to take into consideration when yielding the coroutine and when invoking the handler_type associated with a yield_context to resume the coroutine:
If code is currently running in the coroutine, then it is within the strand associated with the coroutine. Essentially, a simple handler is wrapped by the strand that resumes the coroutine, causing execution to jump to the coroutine, blocking the handler currently in the strand. When the coroutine yields, execution jumps back to the strand handler, allowing it to complete.
While spawn() adds work to the io_service (a handler that will start and jump to the coroutine), the coroutine itself is not work. To prevent the io_service event loop from ending while a coroutine is outstanding, it may be necessary to add work to the io_service before yielding.
Stackful coroutines use a strand to help guarantee the coroutine yields before resume is invoked. Asio 1.10.6 / Boost 1.58 enabled being able to safely invoke the completion handler from within the initiating function. Prior versions required that the completion handler was not invoked from within the initiating function, as its invocation strategy would dispatch(), causing the coroutine to attempt resumption before being suspended.
Here is a complete example that accounts for these details:
#include <iostream> // std::cout, std::endl
#include <chrono> // std::chrono::seconds
#include <functional> // std::bind
#include <thread> // std::thread
#include <utility> // std::forward
#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>
template <typename CompletionToken, typename Signature>
using handler_type_t = typename boost::asio::handler_type<
CompletionToken, Signature>::type;
template <typename Handler>
using async_result = boost::asio::async_result<Handler>;
/// #brief Helper type used to initialize the asnyc_result with the handler.
template <typename CompletionToken, typename Signature>
struct async_completion
{
typedef handler_type_t<CompletionToken, Signature> handler_type;
async_completion(CompletionToken&& token)
: handler(std::forward<CompletionToken>(token)),
result(handler)
{}
handler_type handler;
async_result<handler_type> result;
};
template <typename Signature, typename CompletionToken>
typename async_result<
handler_type_t<CompletionToken, Signature>
>::type
async_func(CompletionToken&& token, boost::asio::io_service& io_service)
{
// The coroutine itself is not work, so guarantee the io_service has
// work.
boost::asio::io_service::work work(io_service);
// Initialize the async completion handler and result.
async_completion<CompletionToken, Signature> completion(
std::forward<CompletionToken>(token));
auto handler = completion.handler;
std::cout << "Spawning thread" << std::endl;
std::thread([](decltype(handler) handler)
{
// 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;
using boost::asio::asio_handler_invoke;
asio_handler_invoke(std::bind(handler, 42), &handler);
}, 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_service io_service;
boost::asio::spawn(io_service,
[&io_service](boost::asio::yield_context yield)
{
auto result = async_func<void(int)>(yield, io_service);
std::cout << "Got: " << result << std::endl;
});
std::cout << "Running" << std::endl;
io_service.run();
std::cout << "Finish" << std::endl;
}
Output:
Running
Spawning thread
Resume coroutine
Suspend coroutine
Got: 42
Finish
For much more details, please consider reading Library Foundations for
Asynchronous Operations. It provides much greater detail into the composition of asynchronous operations, how Signature affects async_result, and the overall design of async_result, handler_type, and async_completion.
Here's an updated example for Boost 1.66.0 based on Tanner's great answer:
#include <iostream> // std::cout, std::endl
#include <chrono> // std::chrono::seconds
#include <functional> // std::bind
#include <thread> // std::thread
#include <utility> // std::forward
#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>
template <typename Signature, typename CompletionToken>
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<CompletionToken, Signature>;
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<void(int)>(yield, 0);
std::cout << "Got: " << result << std::endl; // Got: 1
// add one to one forty one
const auto result2 = async_add_one<void(int)>(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<strand_type>(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<RETURN_TYPE>);
using completion_type = boost::asio::async_completion<yield_context_type, bound_function>;
with yield being:
auto future = completion.result.get();
return future.get(); // may rethrow exception in your coroutine's context
You are complicating things by creating threads out of the executor framework provided by Boost Asio.
For this reason you shouldn't assume that what you want is possible. I strongly suggest just adding more threads to the io_service and letting it manage the strands for you.
Or, you can extend the library and add the new feature you apparently want. If so, it's a good idea to contact the developer mailing list for advice. Perhaps they welcome this feature¹?
¹ (that you, interestingly, have not described, so I won't ask what the purpose of it is)
using CallbackHandler = boost::asio::handler_type<Yield, void (error_code, int)>::type;
void Process(CallbackHandler handler) {
int the_result = 81;
boost::asio::detail::asio_handler_invoke(
std::bind(handler, error_code(), the_result), &handler);
}
Hinted by #sehe, I made the above working solution. But I am not sure if this is the right/idiomatic/best way to do that. Welcome to comment/edit this answer.
What if a basic_waitable_timer is destructed when there are still async operations waiting on it? Is the behavior documented anywhere?
When an IO object, such as basic_waitable_timer, is destroyed, its destructor will invoke destroy() on the IO object's service (not to be confused with the io_service), passing the IO object's implementation. A basic_waitable_timer's service is waitable_timer_service and fulfills the WaitableTimerService type requirement. The WaitableTimerService's requirement defines the post-condition for destroy() to cancel asynchronous wait operations, causing them to complete as soon as possible, and handler's for cancelled operations will be passed the error code boost::asio::error::operation_aborted.
service.destroy(impl); → Implicitly cancels asynchronous wait operations, as if by calling service.cancel(impl, e).
service.cancel(impl, e); → Causes any outstanding asynchronous wait operations to complete as soon as possible. Handlers for cancelled operations shall be passed the error code error::operation_aborted. Sets e to indicate success or failure.
Note that handlers for operations that have already been queued for invocation will not be cancelled and will have an error_code that reflects the success of the operation.
Here is a complete example demonstrating this behavior:
#include <iostream>
#include <boost/asio.hpp>
#include <boost/asio/steady_timer.hpp>
void demo_deferred_completion()
{
std::cout << "[demo deferred completion]" << std::endl;
boost::asio::io_service io_service;
auto wait_completed = false;
// Use scope to force lifetime.
{
// Create the timer and initiate an async_wait operation that
// is guaranteed to have expired.
boost::asio::steady_timer timer(io_service);
// Post a ready-to-run no-op completion handler into the io_service.
// Although the order is unspecified, the current implementation
// will use a predictable order.
io_service.post([]{});
// Initiate an async_wait operation that will immediately expire.
timer.expires_at(boost::asio::steady_timer::clock_type::now());
timer.async_wait(
[&](const boost::system::error_code& error)
{
std::cout << "error: " << error.message() << std::endl;
assert(error == boost::system::error_code()); // Success.
wait_completed = true;
});
// While this will only run one handler (the noop), it will
// execute operations (async_wait), and if they are succesful
// (time expired), the completion handler will be posted for
// deferred completion.
io_service.run_one();
assert(!wait_completed); // Verify the wait handler was not invoked.
} // Destroy the timer.
// Run the handle_wait completion handler.
io_service.run();
assert(wait_completed);
}
void demo_cancelled()
{
std::cout << "[demo cancelled]" << std::endl;
boost::asio::io_service io_service;
// Use scope to force lifetime.
{
boost::asio::steady_timer timer(io_service);
// Initiate an async_wait operation that will immediately expire.
timer.expires_at(boost::asio::steady_timer::clock_type::now());
timer.async_wait(
[](const boost::system::error_code& error)
{
std::cout << "error: " << error.message() << std::endl;
assert(error ==
make_error_code(boost::asio::error::operation_aborted));
});
} // Destroy the timer.
// Run the handle_wait completion handler.
io_service.run();
}
int main()
{
demo_deferred_completion();
demo_cancelled();
}
Output:
[demo deferred completion]
error: Success
[demo cancelled]
error: Operation canceled
It will be canceled: the completion handler is called with an error_code of operation_aborted
Relevant background information: boost::asio async handlers invoked without error after cancellation