55

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?

expert
  • 29,290
  • 30
  • 110
  • 214

2 Answers2

109

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.

Community
  • 1
  • 1
Tanner Sansbury
  • 51,153
  • 9
  • 112
  • 169
  • 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! – expert Oct 10 '12 at 03:10
  • 1
    @ruslan: I have updated the answer to hopefully provide more clarification and detail. To answer your questions for [this](http://stackoverflow.com/revisions/12801042/1) 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. – Tanner Sansbury Oct 10 '12 at 16:09
  • That was a pretty good explanation. Helped me to understand strands better. Thanks @twsansbury – Vikas Oct 10 '12 at 16:43
  • 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`? – Pubby Apr 27 '13 at 15:56
  • 2
    @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`. – Tanner Sansbury Apr 29 '13 at 12:50
  • 6
    I keep coming back to this answer and understanding new slightly subtle insights. This is a remarkably well written answer. – sehe Jun 18 '15 at 14:25
  • Your second "safe" example is identical to the "unsafe" example. Is there a typo somewhere? – CaptainCodeman Jul 29 '15 at 09:47
  • @CaptainCodeman 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. – Tanner Sansbury Jul 29 '15 at 13:06
  • 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. – Fantastory Feb 18 '16 at 08:52
  • @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)? – Victor Mezrin Nov 22 '16 at 02:19
  • 1
    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 http://www.boost.org/doc/libs/1_65_1/doc/html/boost_asio/reference/async_write/overload1.html . Could you tell me the reason execution of async_[receive|write_some] itself unsafe. – Takatoshi Kondo Sep 25 '17 at 05:20
  • 1
    @TakatoshiKondo, it is unsafe because the [`socket`](http://www.boost.org/doc/libs/1_65_1/doc/html/boost_asio/reference/ip__tcp/socket.html) documentation states it is unsafe to use the object in a shared manner. The [Threads and Boost.Asio overview](http://www.boost.org/doc/libs/1_65_1/doc/html/boost_asio/overview/core/threads.html) comments that it is unsafe to make concurrent use of most objects. – Tanner Sansbury Sep 25 '17 at 16:36
  • @Tanner Sansbury, thank you for clarification. I understand that why it is unsafe. – Takatoshi Kondo Sep 26 '17 at 05:37
  • @TakatoshiKondo "it is unsafe because of https://www.boost.org/doc/libs/1_65_1/doc/html/boost_asio/reference/async_write/overload1.html" 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? – John Sep 20 '21 at 02:31
  • @TannerSansbury How to understand "Composed operations are ***unique*** in that intermediate calls to the *stream* are invoked within the *handler*'s strand"? – John Sep 20 '21 at 03:33
  • @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. – John Sep 20 '21 at 03:50
  • 1
    @John I asked about the topic to boost community on a github issue. See https://github.com/boostorg/asio/issues/152 https://www.boost.org/doc/libs/1_65_1/doc/html/boost_asio/reference/ip__tcp/socket.html Shared objects: Unsafe. – Takatoshi Kondo Sep 20 '21 at 08:37
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.

Vikas
  • 8,790
  • 4
  • 38
  • 48
  • The time in hint above is in HH:MM format, I'd suggest to start around [01:08:20](https://youtu.be/D-lTwGJRx0o?t=4100). – Wolf May 15 '23 at 13:20