Boost.Asio: wrapping with different `strand`s several times

643 views Asked by At

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::wraps 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???
        });

    }

};
1

There are 1 answers

0
Jean-Pierre Smith On

What you proposed would not be free of potential race conditions in Boost.Asio (1.65.1). Consider the snippet from your post below,

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

and recall that strand::wrap behaves identically to strand::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).

async_read_until(sock, buf, '\n', [...](...){  // lambda1
    this->strnd.dispatch([...]{                // lambda2
        other->strnd.dispatch([...]{           // lambda3
           other->a ++;
           this->a --:
        });
    });
 });

When async_read_until completes it will invoke the equivalent of lambda1 which will call dispatch on the connections own strand. Whether invoked immediately or later, this will result in lambda2 being invoked in a setting safe for the manipulation of this->a. In lambda2 we invoke other->stnd.dispatch which and by the guarantees of that strand lambda3 will either be invoked immediately or posted to other->strnd. In either case lambda2 completes as does the guarantees for concurrency provided by this->strnd. If lambda3 was posted to other->strnd when it is eventually invoked to access this->a -- we will no long have the guarantees provided for the invocation of lambda2.


Inspecting the headers

We can also see this by inspecting the do_dispatch function in the strand_service (Boost 1.65) with the following counter example, as wrap simply invokes dispatch on the strand when the function is invoked.

Consider a handler wrapped by 2 strands, strand1 and strand2:

func = strand1.wrap(strand2.wrap(some_handler));

and without the underlying io_service being run. Then when func is invoked, as we are not in a current invocation of the strand, dispatch will not immediately invoke the function, but instead request do_dispatch to perform further processing. In do_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 for strand1. Once on the ready-queue the handler is simple invoked once the ready-queue is processed (see do_complete).

This means that the point at which strand2's wrapper is invoked to dispatch on strand2, strand1 is completely done with providing it's guarantees. If strand2'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.