Suppose that I have two connections each having their respective strand
for thread safety. Those connections are not operating alone, and they can talk to each other in some way. During this communication phase, handlers must be synchronized such that, no two handlers can modify connection objects at the same time.
So, in order to achieve that, could I use two strand::wrap
s in a nested way?
For example, consider the following pseudo-code:
class connection /* connection actually uses shared_ptr's to ensure lifetime */
{
public:
connection *other /* somehow set */;
strand strnd /* somehow initialized correctly */;
socket sock /* somehow initialized correctly */;
streambuf buf;
int a /* shared variable */;
void trigger_read() // somewhat triggered
{
// since operations on sock are not thread-safe, use a strand to
// synchronise them
strnd.post([this] {
// now consider the following code,
async_read_until(sock, buf, '\n',
this->strnd.wrap /* wrapping by this->strnd is for sure */([](...) {
// depending on the data, this handler can alter both other
// and this
other->a ++; // not safe
this->a --; // this is safe as protected by this->strnd
}));
// can protect them both by something like,
async_read_until(sock, buf, '\n',
this->strnd.wrap(other->strnd.wrap([](...) {
// depending on the data, this handler can alter both other
// and this
other->a ++; // not safe
this->a --; // this is safe as protected by this->strnd
})));
// this???
});
}
};
What you proposed would not be free of potential race conditions in Boost.Asio (1.65.1). Consider the snippet from your post below,
and recall that
strand::wrap
behaves identically tostrand::dispatch
on invocation. If we consider it in this context, then we can deduce the following. But first let's require the wrapped strands using dispatch and lambdas (for illustration purposes).When
async_read_until
completes it will invoke the equivalent oflambda1
which will call dispatch on the connections own strand. Whether invoked immediately or later, this will result inlambda2
being invoked in a setting safe for the manipulation ofthis->a
. Inlambda2
we invokeother->stnd.dispatch
which and by the guarantees of that strandlambda3
will either be invoked immediately or posted toother->strnd
. In either caselambda2
completes as does the guarantees for concurrency provided bythis->strnd
. Iflambda3
was posted toother->strnd
when it is eventually invoked to accessthis->a --
we will no long have the guarantees provided for the invocation oflambda2
.Inspecting the headers
We can also see this by inspecting the
do_dispatch
function in thestrand_service
(Boost 1.65) with the following counter example, aswrap
simply invokesdispatch
on the strand when the function is invoked.Consider a handler wrapped by 2 strands,
strand1
andstrand2
:and without the underlying
io_service
being run. Then whenfunc
is invoked, as we are not in a current invocation of the strand,dispatch
will not immediately invoke the function, but instead requestdo_dispatch
to perform further processing. Indo_dispatch
since we are not currently running in the I/O service, but no other handler has the strand's lock,do_dispatch
will not signal immediate invocation, but will instead push the handler onto the ready queue forstrand1
. Once on the ready-queue the handler is simple invoked once the ready-queue is processed (seedo_complete
).This means that the point at which
strand2
's wrapper is invoked to dispatch onstrand2
,strand1
is completely done with providing it's guarantees. Ifstrand2
's dispatch call results in immediate invocation then we are fine, if there is a conflict and the handler must be pushed on the waiting queue of strand2 then there is no telling when it will eventually be called.In summary, wrapping a handler in multiple strands does not guarantee that the handler will be in an environment with the concurrency guarantees of all the strands.