Unfortunately, Boost ASIO doesn't have an async_wait_for_condvar()
method.
In most cases, you also won't need it. Programming the ASIO way usually means, that you use strands, not mutexes or condition variables, to protect shared resources. Except for rare cases, which usually focus around correct construction or destruction order at startup and exit, you won't need mutexes or condition variables at all.
When modifying a shared resource, the classic, partially synchronous threaded way is as follows:
- Lock the mutex protecting the resource
- Update whatever needs to be updated
- Signal a condition variable, if further processing by a waiting thread is required
- Unlock the mutex
The fully asynchronous ASIO way is though:
- Generate a message, that contains everything, that is needed to update the resource
- Post a call to an update handler with that message to the resource's strand
- If further processing is needed, let that update handler create further message(s) and post them to the apropriate resources' strands.
- If jobs can be executed on fully private data, then post them directly to the io-context instead.
Here is an example of a class some_shared_resource
, that receives a string state
and triggers some further processing depending on the state received. Please note, that all processing in the private method some_shared_resource::receive_state()
is fully thread-safe, as the strand serializes all calls.
Of course, the example is not complete; some_other_resource
needs a similiar send_code_red()
method as some_shared_ressource::send_state()
.
#include <boost/asio>
#include <memory>
using asio_context = boost::asio::io_context;
using asio_executor_type = asio_context::executor_type;
using asio_strand = boost::asio::strand<asio_executor_type>;
class some_other_resource;
class some_shared_resource : public std::enable_shared_from_this<some_shared_resource> {
asio_strand strand;
std::shared_ptr<some_other_resource> other;
std::string state;
void receive_state(std::string&& new_state) {
std::string oldstate = std::exchange(state, new_state);
if(state == "red" && oldstate != "red") {
// state transition to "red":
other.send_code_red(true);
} else if(state != "red" && oldstate == "red") {
// state transition from "red":
other.send_code_red(false);
}
}
public:
some_shared_resource(asio_context& ctx, const std::shared_ptr<some_other_resource>& other)
: strand(ctx.get_executor()), other(other) {}
void send_state(std::string&& new_state) {
boost::asio::post(strand, [me = weak_from_this(), new_state = std::move(new_state)]() mutable {
if(auto self = me.lock(); self) {
self->receive_state(std::move(new_state));
}
});
}
};
As you see, posting always into ASIO's strands can be a bit tedious at first. But you can move most of that "equip a class with a strand" code into a template.
The good thing about message passing: As you are not using mutexes, you cannot deadlock yourself anymore, even in extreme situations. Also, using message passing, it is often easier to create a high level of parallelity than with classical multithreading. On the downside, moving and copying around all these message objects is time consuming, which can slow down your application.
A last note: Using the weak pointer in the message formed by send_state()
facilitates the reliable destruction of some_shared_resource
objects: Otherwise, if A calls B and B calls C and C calls A (possibly only after a timeout or similiar), using shared pointers instead of weak pointers in the messages would create cyclic references, which then prevents object destruction. If you are sure, that you never will have cycles, and that processing messages from to-be-deleted objects doesn't pose a problem, you can use shared_from_this()
instead of weak_from_this()
, of course. If you are sure, that objects won't get deleted before ASIO has been stopped (and all working threads been joined back to the main thread), then you can also directly capture the this
pointer instead.