Why do I need strand per connection when using boost::asio?
Asked Answered
E

2

56

I'm reviewing HTTP Server 3 example on Boost's website.

Could you guys please explain why I need strand per connection ? As I can see we call read_some only in handler of read-event. So basically read_some calls are sequential therefore there is no need for strand (and item 2 of 3rd paragraph says the same thing). Where is the risk in multi-threading environment?

Estuary answered 9/10, 2012 at 6:33 Comment(1)
"please explain why I need strand per connection?". Where is this recommendation?Iyar
A
110

The documentation is correct. With a half duplex protocol implementation, such as HTTP Server 3, the strand is not necessary. The call chains can be illustrated as follows:

void connection::start()
{
  socket.async_receive_from(..., &handle_read);  ----.
}                                                    |
    .------------------------------------------------'
    |      .-----------------------------------------.
    V      V                                         |
void connection::handle_read(...)                    |
{                                                    |
  if (result)                                        |
    boost::asio::async_write(..., &handle_write); ---|--.
  else if (!result)                                  |  |
    boost::asio::async_write(..., &handle_write);  --|--|
  else                                               |  |
    socket_.async_read_some(..., &handle_read);  ----'  |
}                                                       |
    .---------------------------------------------------'
    |
    V
void handle_write(...)

As shown in the illustration, only a single asynchronous event is started per path. With no possibility of concurrent execution of the handlers or operations on socket_, it is said to be running in an implicit strand.


Thread Safety

While it does not present itself as an issue in the example, I would like to highlight one important detail of strands and composed operations, such as boost::asio::async_write. Before explaining the details, lets first cover the thread safety model with Boost.Asio. For most Boost.Asio objects, it is safe to have multiple asynchronous operations pending on an object; it is just specified that concurrent calls on the object are unsafe. In the diagrams below, each column represents a thread and each line represents what a thread is doing at a moment in time.

It is safe for a single thread to make sequential calls while other threads make none:

 thread_1                             | thread_2
--------------------------------------+---------------------------------------
socket.async_receive(...);            | ...
socket.async_write_some(...);         | ...

It is safe for multiple threads to make calls, but not concurrently:

 thread_1                             | thread_2
--------------------------------------+---------------------------------------
socket.async_receive(...);            | ...
...                                   | socket.async_write_some(...);

However, it is not safe for multiple threads to make calls concurrently1:

 thread_1                             | thread_2
--------------------------------------+---------------------------------------
socket.async_receive(...);            | socket.async_write_some(...);
...                                   | ...

Strands

To prevent concurrent invocations, handlers are often invoked from within strands. This is done by either:

  • Wrapping the handler with strand.wrap. This will return a new handler, that will dispatch through the strand.
  • Posting or dispatching directly through the strand.

Composed operations are unique in that intermediate calls to the stream are invoked within the handler's strand, if one is present, instead of the strand in which the composed operation is initiated. When compared to other operations, this presents an inversion of where the strand is specified. Here is some example code focusing on strand usage, that will demonstrate a socket that is read from via a non-composed operation, and concurrently written to with a composed operation.

void start()
{
  // Start read and write chains.  If multiple threads have called run on
  // the service, then they may be running concurrently.  To protect the
  // socket, use the strand.
  strand_.post(&read);
  strand_.post(&write);
}

// read always needs to be posted through the strand because it invokes a
// non-composed operation on the socket.
void read()
{
  // async_receive is initiated from within the strand.  The handler does
  // not affect the strand in which async_receive is executed.
  socket_.async_receive(read_buffer_, &handle_read);
}

// This is not running within a strand, as read did not wrap it.
void handle_read()
{
  // Need to post read into the strand, otherwise the async_receive would
  // not be safe.
  strand_.post(&read);
}

// The entry into the write loop needs to be posted through a strand.
// All intermediate handlers and the next iteration of the asynchronous write
// loop will be running in a strand due to the handler being wrapped.
void write()
{
  // async_write will make one or more calls to socket_.async_write_some.
  // All intermediate handlers (calls after the first), are executed
  // within the handler's context (strand_).
  boost::asio::async_write(socket_, write_buffer_,
                           strand_.wrap(&handle_write));
}

// This will be invoked from within the strand, as it was a wrapped
// handler in write().
void handle_write()
{
  // handler_write() is invoked within a strand, so write() does not
  // have to dispatched through the strand.
  write();
}

Importance of Handler Types

Also, within composed operations, Boost.Asio uses argument dependent lookup (ADL) to invoke intermediate handlers through the completion handler's strand. As such, it is important that the completion handler's type has the appropriate asio_handler_invoke() hooks. If type erasure occurs to a type that does not have the appropriate asio_handler_invoke() hooks, such as a case where a boost::function is constructed from the return type of strand.wrap, then intermediate handlers will execute outside of the strand, and only the completion handler will execute within the strand. See this answer for more details.

In the following code, all intermediate handlers and the completion handler will execute within the strand:

boost::asio::async_write(stream, buffer, strand.wrap(&handle_write));

In the following code, only the completion handler will execute within the strand. None of the intermediate handlers will execute within the strand:

boost::function<void()> handler(strand.wrap(&handle_write));
boost::asio::async_write(stream, buffer, handler);

1. The revision history documents an anomaly to this rule. If supported by the OS, synchronous read, write, accept, and connection operations are thread safe. I an including it here for completeness, but suggest using it with caution.

Alchemize answered 9/10, 2012 at 13:22 Comment(17)
Could you please elaborate more one second part of your answer ? I feel like it's something important to understand but I'm not sure I understand it clearly. Perhaps I have two questions: 1) How would you invoke second write() to make it run through strand? 2) Does it mean strand_.wrap in second example is useless? Thanks!Estuary
@ruslan: I have updated the answer to hopefully provide more clarification and detail. To answer your questions for this revision, you could have write be invoked within the strand by using strand.post or strand.dispatch, such as with strand.post( boost::bind( write ) ). Also, the strand_.wrap in the second example would only be useful if handle_write used resources that need to be synchronized.Alchemize
That was a pretty good explanation. Helped me to understand strands better. Thanks @twsansburyMaffick
For clarification, strand_one.post([]{ async_write(socket_, write_buffer_, strand_two.wrap(&handle_write)) }); will execute the async_write in strand_two, but strand_one.post([]{ async_read_some(socket_, read_buffer_, strand_two.wrap(&handle_read)) }); will execute in strand_one?Grosso
@Pubby: With both of those examples, the first operation will execute within strand_one, while all intermediate handlers and the completion handler execute within strand_two.Alchemize
I keep coming back to this answer and understanding new slightly subtle insights. This is a remarkably well written answer.Ab
Your second "safe" example is identical to the "unsafe" example. Is there a typo somewhere?Chamfer
@Chamfer The diagram attempts to illustrate concurrency by having each line represent a period of time for both threads. In the unsafe example, both calls occur on the same line/time. In the other examples, they occur on separate lines/time. I have updated the answer in hopes that it provides clarification.Alchemize
I was not able to fix composed operation problem of async_write with strand. I ended with manually managed buffers - single buffer for single socket.Gravitt
@TannerSansbury For the conclusion - is it enough to have one strand for each ssl::stream and one strand for a ssl::context (which is shared among threads)?Spates
The answer said "However, it is not safe for multiple threads to make calls concurrently:", that means doing socket.async_receive(...); and socket.async_write_some(...); concurrently is unsafe. Why it is unsafe? I couldn't find the description about that in Boost.Asio document. I understand that if the completion handler of the socket.async_receive(...); calls socket.async_write_some(...); , it is unsafe because of boost.org/doc/libs/1_65_1/doc/html/boost_asio/reference/… . Could you tell me the reason execution of async_[receive|write_some] itself unsafe.Tanh
@TakatoshiKondo, it is unsafe because the socket documentation states it is unsafe to use the object in a shared manner. The Threads and Boost.Asio overview comments that it is unsafe to make concurrent use of most objects.Alchemize
@Tanner Sansbury, thank you for clarification. I understand that why it is unsafe.Tanh
@TakatoshiKondo "it is unsafe because of boost.org/doc/libs/1_65_1/doc/html/boost_asio/reference/…" Question:I really can't see any relation between it is unsafe and boost.org/doc/libs/1_65_1/doc/html/boost_asio/reference/…. Could you please explain that in more detail for me?Misprize
@TannerSansbury How to understand "Composed operations are unique in that intermediate calls to the stream are invoked within the handler's strand"?Misprize
@TannerSansbury "async_receive() always needs to be posted through the strand because it invokes a non-composed operation on the socket."Reply:I really can't any relation between "async_receive() always needs to be posted through the strand " and " it invokes a non-composed operation on the socket.". Could you please explain that in more detail for me? Thanks.Misprize
@Misprize I asked about the topic to boost community on a github issue. See github.com/boostorg/asio/issues/152 boost.org/doc/libs/1_65_1/doc/html/boost_asio/reference/ip__tcp/… Shared objects: Unsafe.Tanh
M
10

I believe it is because the composed operation async_write. async_write is composed of multiple socket::async_write_some asynchronously. Strand is helpful to serialize those operations. Chris Kohlhoff, the author of asio, talks about it briefly in his boostcon talk at around 1:17.

Maffick answered 9/10, 2012 at 7:9 Comment(1)
The time in hint above is in HH:MM format, I'd suggest to start around 01:08:20.Hilbert

© 2022 - 2024 — McMap. All rights reserved.