使用boost :: asio时,为什么每个连接需要strand?

exp*_*ert 43 c++ boost boost-asio

我正在审查Boost网站上的HTTP Server 3示例.

你能解释我为什么需要strand每个连接吗?正如我所看到的,我们read_some只在read-event的处理程序中调用.所以基本上read_some调用是顺序的,因此不需要strand(第3段的第2项说同样的事情).多线程环境中的风险在哪里?

Tan*_*ury 88

文档是正确的.使用半双工协议实现,例如HTTP Server 3,strand不是必需的.调用链可以说明如下:

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(...)
Run Code Online (Sandbox Code Playgroud)

如图所示,每个路径只启动一个异步事件.由于不可能同时执行处理程序或操作socket_,因此它被称为在隐式链中运行.


线程安全

虽然它不会在示例中出现问题,但我想强调一下股线和组合操作的一个重要细节,例如boost::asio::async_write.在解释细节之前,让我们首先用Boost.Asio覆盖线程安全模型.对于大多数Boost.Asio对象,在对象上挂起多个异步操作是安全的.它只是指定对象的并发调用是不安全的.在下图中,每列代表一个线程,每一行代表一个线程在某个时刻正在做什么.

单个线程进行顺序调用是安全的,而其他线程没有:

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

多个线程可以安全地进行调用,但不能同时进行:

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

但是,多个线程同时进行调用是不安全的1:

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

为了防止并发调用,通常从内部调用处理程序.这可以通过:

  • 包装处理程序strand.wrap.这将返回一个新的处理程序,它将通过strand分派.
  • 直接通过股线发布发送.

组合操作的独特之处在于,在处理程序的链中调用对流的中间调用(如果存在),而不是启动组合操作的链.与其他操作相比,这表明了指定链的位置的反转.下面是一些关注链使用的示例代码,它将演示通过非组合操作读取的套接字,并通过组合操作同时写入.

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();
}
Run Code Online (Sandbox Code Playgroud)

处理程序类型的重要性

此外,在组合操作中,Boost.Asio使用参数依赖查找(ADL)通过完成处理程序的链调用中间处理程序.因此,完成处理程序的类型具有适当的asio_handler_invoke()钩子是很重要的.如果类型擦除发生在没有适当asio_handler_invoke()钩子的类型上,例如a boost::function是从返回类型构造的情况strand.wrap,那么中间处理程序将在strand之外执行,并且只有完成处理程序将在strand中执行.有关详细信息,请参阅答案.

在以下代码中,所有中间处理程序和完成处理程序将在strand中执行:

boost::asio::async_write(stream, buffer, strand.wrap(&handle_write));
Run Code Online (Sandbox Code Playgroud)

在以下代码中,只有完成处理程序将在strand中执行.没有任何中间处理程序将在链中执行:

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

1. 修订历史记录了此规则的异常.如果操作系统支持,则同步读取,写入,接受和连接操作是线程安全的.为了完整起见,我在这里包括它,但建议谨慎使用它.

  • 我一直回到这个答案并理解新的略微微妙的见解.这是一个非常好的答案. (4认同)
  • @Pubby:对于这两个例子,第一个操作将在`strand_one`中执行,而所有中间处理程序和完成处理程序在`strand_two`中执行. (2认同)

Vik*_*kas 7

我相信这是因为组合操作async_write.async_write由异步多个socket :: async_write_some组成.Strand有助于序列化这些操作.asio的作者克里斯·科尔霍夫(Chris Kohlhoff)在1:17 左右的简短谈话中简要谈到了这一点.