Consider an echo server implemented using Boost.asio. Read events from connected clients result in blocks of data being placed on to an arrival event queue. A pool of thread
A strand is an execution context which executes handlers within a critical section, on a correct thread.
That critical section is implemented (more or less) with a mutex.
It's a little cleverer than that because if a dispatcher detects that a thread is already in the strand, it appends the handler to a queue of handlers to be executed before the critical section has been left, but after the current handler has completed.
thus in this case the new handler is 'sort of' posted to the currently executing thread.
There are some guarantees in ordering.
strand::post/dispatch(x);
strand::post/dispatch(y);
will always result in x happening before y.
but if x dispatches a handler z during its execution, then the execution order will be:
x, z, y
note that the idiomatic way to handle io completion handlers with strands is not to post work to a strand in the completion handler, but to wrap the completion handler in the strand, and do the work there.
asio contains code to detect this and will do the right thing, ensuring correct ordering and eliding un-necessary intermediate posts.
e.g.:
async_read(sock, mystrand.wrap([](const auto& ec, auto transferred)
{
// this code happens in the correct strand, in the correct order.
});
strand
provides a guarantee for non-concurrency and the invocation order of handlers; strand
does not control the order in which operations are executed and demultiplexed. Use a strand
if you have either:
The io_service
will provide the desired and expected ordering of buffers being filled or used in the order in which operations are initiated. For instance, if the socket
has "Strawberry fields forever." available to be read, then given:
buffer1.resize(11); // buffer is a std::vector managed elsewhere
buffer2.resize(7); // buffer is a std::vector managed elsewhere
buffer3.resize(8); // buffer is a std::vector managed elsewhere
socket.async_read_some(boost::asio::buffer(buffer1), handler1);
socket.async_read_some(boost::asio::buffer(buffer2), handler2);
socket.async_read_some(boost::asio::buffer(buffer3), handler3);
When the operations complete:
handler1
is invoked, buffer1
will contain "Strawberry "handler2
is invoked, buffer2
will contain "fields "handler3
is invoked, buffer3
will contain "forever."However, the order in which the completion handlers are invoked is unspecified. This unspecified ordering remains true even with a strand
.
Asio uses the Proactor design pattern[1] to demultiplex operations. On most platforms, this is implemented in terms of a Reactor. The official documentation mentions the components and their responsibilities. Consider the following example:
socket.async_read_some(buffer, handler);
The caller is the initiator, starting an async_read_some
asynchronous operation and creating the handler
completion handler. The asynchronous operation is executed by the StreamSocketService operation processor:
handler
completion handler into the io_service
io_service
is ran and data is available on the socket, then the reactor will inform Asio. Next, Asio will dequeue an outstanding read operation from the socket, execute it, and enqueue the handler
completion handler into the io_service
The io_service
proactor will dequeue a completion handler, demultiplex the handler to threads that are running the io_service
, from which the handler
completion handler will be executed. The order of invocation of the completion handlers is unspecified.
If multiple operations of the same type are initiated on a socket, it is currently unspecified as to the order in which the buffers will be used or filled. However, in the current implementation, each socket uses a FIFO queue for each type of pending operation (e.g. a queue for read operations; a queue for write operations; etc). The networking-ts draft, which is based partially on Asio, specifies:
the
buffers
are filled in the order in which these operations were issued. The order of invocation of the completion handlers for these operations is unspecified.
Given:
socket.async_read_some(buffer1, handler1); // op1
socket.async_read_some(buffer2, handler2); // op2
As op1
was initiated before op2
, then buffer1
is guaranteed to contain data that was received earlier in the stream than the data contained in buffer2
, but handler2
may be invoked before handler1
.
Composed operations are composed of zero or more intermediate operations. For example, the async_read() composed asynchronous operation is composed of zero or more intermediate stream.async_read_some()
operations.
The current implementation uses operation chaining to create a continuation, where a single async_read_some()
operation is initiated, and within its internal completion handle, it determines whether or not to initiate another async_read_some()
operation or to invoke the user's completion handler. Because of the continuation, the async_read
documentation requires that no other reads occur until the composed operation completes:
The program must ensure that the stream performs no other read operations (such as
async_read
, the stream'sasync_read_some
function, or any other composed operations that perform reads) until this operation completes.
If a program violates this requirement, one may observe interwoven data, because of the aforementioned order in which buffers are filled.
For a concrete example, consider the case where an async_read()
operation is initiated to read 26 bytes of data from a socket:
buffer.resize(26); // buffer is a std::vector managed elsewhere
boost::asio::async_read(socket, boost::asio::buffer(buffer), handler);
If the socket receives "Strawberry ", "fields ", and then "forever.", then the async_read()
operation may be composed of one or more socket.async_read_some()
operations. For instance, it could be composed of 3 intermediate operations:
async_read_some()
operation reads 11 bytes containing "Strawberry " into the buffer starting at an offset of 0. The completion condition of reading 26 bytes has not been satisfied, so another async_read_some()
operation is initiated to continue the operationasync_read_some()
operation reads 7 byes containing "fields " into the buffer starting at an offset of 11. The completion condition of reading 26 bytes has not been satisfied, so another async_read_some()
operation is initiated to continue the operationasync_read_some()
operation reads 8 byes containing "forever." into the buffer starting at an offset of 18. The completion condition of reading 26 bytes has been satisfied, so handler
is enqueued into the io_service
When the handler
completion handler is invoked, buffer
contains "Strawberry fields forever."
strand is used to provide serialized execution of handlers in a guaranteed order. Given:
s
f1
that is added to strand s
via s.post()
, or s.dispatch()
when s.running_in_this_thread() == false
f2
that is added to strand s
via s.post()
, or s.dispatch()
when s.running_in_this_thread() == false
then the strand provides a guarantee of ordering and non-concurrency, such that f1
and f2
will not be invoked concurrently. Furthermore, if the addition of f1
happens before the addition of f2
, then f1
will be invoked before f2
.
With:
auto wrapped_handler1 = strand.wrap(handler1);
auto wrapped_handler2 = strand.wrap(handler2);
socket.async_read_some(buffer1, wrapped_handler1); // op1
socket.async_read_some(buffer2, wrapped_handler2); // op2
As op1
was initiated before op2
, then buffer1
is guaranteed to contain data that was received earlier in the stream than the data contained in buffer2
, but the order in which the wrapped_handler1
and wrapped_handler2
will be invoked is unspecified. The strand
guarantees that:
handler1
and handler2
will not be invoked concurrentlywrapped_handler1
is invoked before wrapped_handler2
, then handler1
will be invoked before handler2
wrapped_handler2
is invoked before wrapped_handler1
, then handler2
will be invoked before handler1
Similar to the composed operation implementation, the strand
implementation uses operation chaining to create a continuation. The strand
manages all handlers posted to it in a FIFO queue. When the queue is empty and a handler is posted to the strand, then the strand will post an internal handle to the io_service
. Within the internal handler, a handler will be dequeued from the strand
's FIFO queue, executed, and then if the queue is not empty, the internal handler posts itself back to the io_service
.
Consider reading this answer to find out how a composed operation uses asio_handler_invoke() to wrap intermediate handlers within the same context (i.e. strand
) of the completion handler. The implementation details can be found in the comments on this question.
1. [POSA2] D. Schmidt et al, Pattern Oriented Software Architecture, Volume 2. Wiley, 2000.