Strands: Use Threads Without Explicit Locking

Posted on Sun 05 June 2022 in Journal

io_service::strand

Provides serialised handler execution.

The io_service::strand class provides the ability to post and dispatch handlers with the guarantee that none of those handlers will execute concurrently.

Order of handler invocation

Given:

  • a strand object s
  • an object a meeting completion handler requirements
  • an object a1 which is an arbitrary copy of a made by the implementation
  • an object b meeting completion handler requirements
  • an object b1 which is an arbitrary copy of b made by the implementation

if any of the following conditions are true:

  • s.post(a) happens-before s.post(b)
  • s.post(a) happens-before s.dispatch(b), where the latter is performed outside the strand
  • s.dispatch(a) happens-before s.post(b), where the former is performed outside the strand
  • s.dispatch(a) happens-before s.dispatch(b), where both are performed outside the strand

then asio_handler_invoke(a1, &a1) happens-before asio_handler_invoke(b1, &b1).

Note that in the following case:

async_op_1(..., s.wrap(a));
async_op_2(..., s.wrap(b));

the completion of the first async operation will perform s.dispatch(a), and the second will perform s.dispatch(b), but the order in which those are performed is unspecified. That is, you cannot state whether one happens-before the other. Therefore none of the above conditions are met and no ordering guarantee is made.

"上面千条线、下面一根针", strand 就是这样一根针,或者翻译为链,它可以被定义为一个对于若干事件处理器(event handlers)的严格的顺序调用, 也就是没有并发调用。

strand 的使用允许在多线程程序中无需显式加锁而执行代码。

  1. 调用 io_context::run()

Strands 可以用以下的方法显式或隐式地使用

  • 只从一个线程来调用 io_context::run() 意味着所有的 event handler 在一个隐含的 strand 中执行 由于 io_context 保证 handlers 只会在 run() 内调用

  • 当有一个单个的异步操作链与一个连接关联(例如在一个半双工协议实现中,比如 HTTP), 不可并发执行 handlers, 这也是一个隐含的 strand

  • 一个显式的 strand 是一个 strand<> 或者 io_context::strand 的实例. 所有的 event handler 函数对象需要通过 boost::asio::bind_executor() 来绑定到这个 strand 上,或者通过 strand 对象的 post/dispatch 方法来绑定

在组合异步操作的情况下,例如 async_read()async_read_until(),如果完成处理程序通过一个 strand,那么所有中间处理程序也应该通过同一个 strand。 这需要确保对调用者和组合操作之间共享的任何对象的线程安全访问 (在 async_read() 的情况下,它是套接字,调用者可以 close() 取消操作)。

为此,所有异步操作都可通过使用 get_associated_executor 函数获取处理程序的关联执行程序。 例如:

boost::asio::associated_executor_t<Handler> a = boost::asio::get_associated_executor(h);

关联的执行者必须满足执行者的要求。 异步操作将使用它来提交中间和最终处理程序以供执行。

可以通过指定嵌套类型 executor_type 和成员函数 get_executor() 来为特定的处理程序类型定制执行程序:

class my_handler
{
public:
  // Custom implementation of Executor type requirements.
  typedef my_executor executor_type;

  // Return a custom executor implementation.
  executor_type get_executor() const noexcept
  {
    return my_executor();
  }

  void operator()() { ... }
};

在更复杂的情况下, associated_executor 模板可以直接部分特化:

struct my_handler
{
  void operator()() { ... }
};

namespace boost { namespace asio {

  template <class Executor>
  struct associated_executor<my_handler, Executor>
  {
    // Custom implementation of Executor type requirements.
    typedef my_executor type;

    // Return a custom executor implementation.
    static type get(const my_handler&,
        const Executor& = Executor()) noexcept
    {
      return my_executor();
    }
  };

} } // namespace boost::asio

The boost::asio::bind_executor() function is a helper to bind a specific executor object, such as a strand, to a completion handler. This binding automatically associates an executor as shown above. For example, to bind a strand to a completion handler we would simply write:

boost::asio::bind_executor() 函数是将特定的 executor 对象(例如 strand)绑定到完成处理程序的一个辅助函数。 这个绑定会自动关联一个executor,如上所示。 例如,要将 strand 绑定到完成处理程序,我们只需编写:

my_socket.async_read_some(my_buffer,
    boost::asio::bind_executor(my_strand,
      [](error_code ec, size_t length)
      {
        // ...
      }));

本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。