什么时候必须将 io_context 传递给 boost::asio::spawn? (C++)

When must you pass io_context to boost::asio::spawn? (C++)

我惊讶地发现下面的代码在没有将 io_context 作为第一个参数传递给 spawn 的情况下工作。有人可以解释为什么在这种情况下我不需要传递它,以及在什么情况下您必须明确传递它。 我正在使用 Boost 1.75.0

#include <boost/asio/spawn.hpp>
#include <boost/asio/deadline_timer.hpp>
#include <iostream>

int main() {

  boost::asio::io_context io_context;
  boost::asio::deadline_timer timer(io_context);

  boost::asio::spawn([&](boost::asio::yield_context yield){ // don't need to pass io_context?!
    std::cout << "started spawn" << std::endl;
    timer.expires_from_now(boost::posix_time::seconds(5));
    timer.async_wait(yield);
    std::cout << "finished spawn" << std::endl;
  });

  std::cout << "running io_context" << std::endl;
  io_context.run();
  std::cout << "finished running io_context" << std::endl;

}

Asio 添加了关联执行器默认执行器 的概念。

关联的执行器并不是真正新的,因为 handler_invoke 协议已经允许处理程序类型特定的语义。然而,自从执行者概念的制定以来,它变得更加普遍。

现在您可以post任何处理程序,它将在关联的执行程序上执行,执行程序提供默认执行程序。默认执行器最终是system_executor{}.

所以

post([]{ puts("Hello world"); });
post(system_executor{}, []{ puts("Hello world"); });

两个处理程序都使用 system_executor

您可以将关联的处理程序与任何尚未关联的处理程序绑定:

post(bind_executor(ex1, []{ puts("Hello world"); }));
post(system_executor{}, bind_executor(ex1, []{ puts("Hello world"); }));

运行 ex1 上的处理程序,而不是回退。结合以上内容,您已经期望这样做是一样的:

post(ex1, []{ puts("Hello world"); });

(这里handler没有关联executor,所以ex1起到fallback的作用)

重生

Spawn 只是“发布”另一种处理程序的包装器¹。事实上,使用任何关联的执行程序都有记录。该实现非常清楚地反映了这一点:

template <typename Function>
inline void spawn(BOOST_ASIO_MOVE_ARG(Function) function,
    const boost::coroutines::attributes& attributes)
{
  typedef typename decay<Function>::type function_type;

  typename associated_executor<function_type>::type ex(
      (get_associated_executor)(function));

  boost::asio::spawn(ex, BOOST_ASIO_MOVE_CAST(Function)(function), attributes);
}

您可以看到调用 get_associated_executor 时没有显式回退,再次默认为 system_executor

旁注

此外

  • spawn 将在适当的地方添加一个链(这就是为什么在构建执行上下文时提供并发提示会产生很大差异的原因)
  • spawn 可以将 yield_context 作为第一个参数,在这种情况下,您将有效地 运行 在同一链上(共享执行程序)

¹ 这是一个实现细节,但通常是 boost::asio::detail::spawn_helper<...>,它会再次正确传播关联的 executors/allocators。我将这种类型称为“处理程序活页夹”

现场演示

为了说明正在使用 system_executor 的现实,这里有一个简化的测试器:

Compiler Explorer

#include <boost/asio/spawn.hpp>
#include <boost/asio/steady_timer.hpp>
#include <iostream>

int main() {
    using namespace boost::asio;
    using namespace std::chrono_literals;
    io_context ctx(1);

    spawn([](yield_context yield) {
        std::cout << "started spawn" << std::endl;

        auto ex = get_associated_executor(yield);
        //auto work = make_work_guard(ex);

        steady_timer timer(ex, 5s);
        timer.async_wait(yield);

        std::cout << "finished spawn" << std::endl;
    });

    std::cout << "running context" << std::endl;
    query(system_executor{}, execution::context).join();
    std::cout << "finished running context" << std::endl;
}

备注:

  • ctx 现在接受并发提示(如上所述)

  • ctx 从未使用过;加入它会不会等待coro完成!

  • 注意注释 work。重要的是,虽然异步操作构成工作,但 Coro itself is not work 因此在某些情况下您可能希望保护 coro 的范围。

  • 请注意,system_executor 的加入方式与其他基于线程的执行上下文类似 thread_pool:

     query(system_executor{}, execution::context).join();
    

现在打印

started spawn
running context
finished spawn
finished running context

如预期延迟 5 秒。