为啥在使用 boost::asio 时每个连接都需要 strand?

Posted

技术标签:

【中文标题】为啥在使用 boost::asio 时每个连接都需要 strand?【英文标题】:Why do I need strand per connection when using boost::asio?为什么在使用 boost::asio 时每个连接都需要 strand? 【发布时间】:2012-09-29 10:25:54 【问题描述】:

我正在查看 Boost 网站上的 HTTP Server 3 示例。

请你们解释一下为什么我需要strand 每个连接?正如我所看到的,我们只在读取事件的处理程序中调用read_some。所以基本上read_some 调用是连续的,因此不需要 strand(item 2 of 3rd paragraph 说同样的话)。多线程环境的风险在哪里?

【问题讨论】:

“请解释为什么我需要每个连接的链?”。这个建议在哪里? 【参考方案1】:

文档是正确的。对于半双工协议实现,例如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(...)

如图所示,每个路径只启动一个异步事件。在socket_ 上不可能同时执行处理程序或操作,据说它在隐式链中运行。


线程安全

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

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

 thread_1 |线程_2
--------------------------------------------------+------------ ----------------------------
socket.async_receive(...); | ...
socket.async_write_some(...); | ...

多线程调用是安全的,但不是并发的:

 thread_1 |线程_2
--------------------------------------------------+------------ ----------------------------
socket.async_receive(...); | ...
... | socket.async_write_some(...);

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

 thread_1 |线程_2
--------------------------------------------------+------------ ----------------------------
socket.async_receive(...); | socket.async_write_some(...);
... | ...

为了防止并发调用,处理程序通常从链中调用。这由以下任一方式完成:

strand.wrap 包装处理程序。这将返回一个新的处理程序,它将通过 strand 进行调度。 Posting 或 dispatching 直接通过链。

组合操作的独特之处在于,对 的中间调用是在 handler 的链中调用的,如果存在的话,而不是在其中组合的链中调用。启动操作。与其他操作相比,这呈现了指定链的位置的倒置。这是一些专注于 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();


处理程序类型的重要性

此外,在组合操作中,Boost.Asio 使用argument dependent lookup (ADL) 通过完成处理程序的链调用中间处理程序。因此,完成处理程序的类型具有适当的asio_handler_invoke() 挂钩非常重要。如果类型擦除发生在没有适当的asio_handler_invoke() 钩子的类型上,例如boost::function 是从strand.wrap 的返回类型构造的,那么中间处理程序将在链外执行,并且仅完成处理程序将在链中执行。更多详情见this答案。

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

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

在下面的代码中,只有完成处理程序会在链中执行。没有一个中间处理程序将在链中执行:

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

1。 revision history 记录了此规则的异常情况。如果操作系统支持,同步读取、写入、接受和连接操作是线程安全的。为了完整起见,我将其包含在此处,但建议谨慎使用。

【讨论】:

@ruslan:我已经更新了答案,希望能提供更多的说明和细节。要回答有关this 修订版的问题,您可以使用strand.poststrand.dispatch 在链中调用write,例如使用strand.post( boost::bind( write ) )。此外,第二个示例中的strand_.wrap 仅在handle_write 使用需要同步的资源时才有用。 @Pubby:对于这两个示例,第一个操作将在strand_one 内执行,而所有中间处理程序和完成处理程序都在strand_two 内执行。 我不断回到这个答案并理解新的稍微微妙的见解。这是一个写得非常好的答案。 答案是“但是,多个线程并发调用是不安全的:”,这意味着做socket.async_receive(...);和 socket.async_write_some(...);同时是不安全的。为什么不安全?我在 Boost.Asio 文档中找不到关于它的描述。我知道如果 socket.async_receive(...); 的完成处理程序调用 socket.async_write_some(...); , 因为 boost.org/doc/libs/1_65_1/doc/html/boost_asio/reference/… 是不安全的。你能告诉我执行 async_[receive|write_some] 本身不安全的原因吗? @TakatoshiKondo,这是不安全的,因为socket 文档指出以共享方式使用该对象是不安全的。 Threads and Boost.Asio overview 认为并发使用大多数对象是不安全的。【参考方案2】:

我相信是因为组合操作async_write。 async_write由多个socket::async_write_some异步组成。 Strand 有助于序列化这些操作。 asio 的作者 Chris Kohlhoff 在他的boostcon talk 1:17 左右简短地谈到了它。

【讨论】:

boostcon talk 的链接不再可用。

以上是关于为啥在使用 boost::asio 时每个连接都需要 strand?的主要内容,如果未能解决你的问题,请参考以下文章

boost::asio::ip::tcp::acceptor 在使用 async_accept 接收连接请求时终止应用程序

为啥 boost::asio::read 缓冲区数据大小小于读取大小?

boost.asio中使用协程

为啥 io_context 出现在我的 boost asio 协程服务器中

boost::asio 完全断开连接

使用 boost::asio::thread_pool 的 C++ 线程池,为啥我不能重用我的线程?