prefer、require 和 make_work_guard 之间的 Asio 区别

Asio difference between prefer, require and make_work_guard

在下面的示例中,我为我的应用程序启动了一个工作线程。后来我 post 做了一些工作。为了防止它过早返回,我必须确保“工作”出色。我使用 work_guard 对象执行此操作。但是我发现了另外两种“确保”工作的方法。我应该在整个申请过程中使用哪一个?有区别吗?

#include <boost/asio.hpp>
#include <boost/thread.hpp>
#include <syncstream>
#include <iostream>

namespace asio = boost::asio;

int main() {
  asio::io_context workerIO;
  boost::thread workerThread;
  {
    // ensure the worker io context stands by until work is posted at a later time
    // one of the below is needed for the worker to execute work which one should I use?
    auto prodWork = asio::make_work_guard(workerIO);
    // prodWork.reset(); // can be cleared
    asio::any_io_executor prodWork2 = asio::prefer(workerIO.get_executor(), asio::execution::outstanding_work_t::tracked);
    // prodWork2 = asio::any_io_executor{}; // can be cleared
    asio::any_io_executor prodWork3 = asio::require(workerIO.get_executor(), asio::execution::outstanding_work_t::tracked);
    // prodWork3 = asio::any_io_executor{}; // can be cleared

    workerThread = boost::thread{[&workerIO] {
      std::osyncstream(std::cout) << "Worker RUN START: " << std::hash<std::thread::id>{}(std::this_thread::get_id()) << std::endl;
      workerIO.run();
      std::osyncstream(std::cout) << "Worker RUN ENDED: " << std::hash<std::thread::id>{}(std::this_thread::get_id()) << std::endl;
    }};
    asio::io_context appIO;


    std::osyncstream(std::cout) << "Main RUN START: " << std::hash<std::thread::id>{}(std::this_thread::get_id()) << std::endl;

    // schedule work here
    auto timer = asio::steady_timer{appIO};
    timer.expires_after(std::chrono::seconds(4));
    timer.async_wait([&workerIO] (auto ec) {
      std::osyncstream(std::cout) << "Main: timer expired " << std::hash<std::thread::id>{}(std::this_thread::get_id()) << std::endl;
      asio::post(workerIO.get_executor(), [] {
        std::osyncstream(std::cout) << "Worker WORK DONE " << std::hash<std::thread::id>{}(std::this_thread::get_id()) << std::endl;
      });
      std::osyncstream(std::cout) << "Main: work posted to worker " << std::hash<std::thread::id>{}(std::this_thread::get_id()) << std::endl;
    });

    appIO.run();
    std::osyncstream(std::cout) << "Main RUN ENDED: " << std::hash<std::thread::id>{}(std::this_thread::get_id()) << std::endl;
  }
  workerThread.join(); // wait for the worker to finish its posted work
  std::osyncstream(std::cout) << "Main EXIT: " << std::hash<std::thread::id>{}(std::this_thread::get_id()) << std::endl;
  return 0;
}

是否有关于如何以及何时使用 asio::prefer 和 asio::require 的示例?

文档:

https://www.boost.org/doc/libs/develop/doc/html/boost_asio/reference/io_context.html https://www.boost.org/doc/libs/1_78_0/doc/html/boost_asio/reference/executor_work_guard.html

我的知识来自于例如WG22 P0443R12 "A Unified Executors Proposal for C++".

前面的一些差异:work-guard

  • 不会改变执行器,而只是在其上调用 on_work_started()on_work_finished()。 [可能有一个执行者对这两个都没有影响。]
  • 可以 reset() 独立于其生命周期,或任何执行程序实例的生命周期。解耦生命周期是一个特性。

另一方面,使用prefer/require应用outstanding_work sub-properties:

  • 修改现有执行器
  • 特别是在复制时,所有副本都将具有相同的属性。对于像保持执行 context/resources 这样具有侵入性的事情来说,这可能是危险的。

扫描字段

然而,并不是所有的属性都是必需的。使用定义为 Ex

进行一些侦察
using namespace boost::asio::execution;
using boost::asio::io_context;
using Ex = io_context::executor_type;

首先,检查哪些属性甚至可以是 required/preferred(显示的所有静态断言都通过了):

using namespace boost::asio::execution;
static_assert(not outstanding_work_t::is_preferable);
static_assert(not outstanding_work_t::is_requirable);
static_assert(outstanding_work_t::untracked_t::is_preferable);
static_assert(outstanding_work_t::tracked_t::is_requirable);

到目前为止一切顺利。我们还要检查属性是否适用于 Ex:

static_assert(boost::asio::is_applicable_property_v<Ex, outstanding_work_t::tracked_t>);
static_assert(boost::asio::is_applicable_property_v<Ex, outstanding_work_t::untracked_t>);

非常好。观察它的默认值是未跟踪的:

static_assert(outstanding_work.static_query<Ex>() != outstanding_work.tracked);
static_assert(outstanding_work.static_query<Ex>() == outstanding_work.untracked);

的确,要求 untracked 不会更改执行程序类型,但要求 tracked 会:

boost::asio::io_context ioc;

using boost::asio::require;
auto ex           = ioc.get_executor();
auto untracked_ex = require(ex, outstanding_work.untracked);
auto tracked_ex   = require(ex, outstanding_work.tracked);

static_assert(std::is_same_v<Ex, decltype(untracked_ex)>);
static_assert(not std::is_same_v<Ex, decltype(tracked_ex)>);

不同之处在于 io_context::basic_executor_typeBits 模板参数变为 4,这确实符合预期:

  struct io_context_bits
  {
    BOOST_ASIO_STATIC_CONSTEXPR(uintptr_t, blocking_never = 1);
    BOOST_ASIO_STATIC_CONSTEXPR(uintptr_t, relationship_continuation = 2);
    BOOST_ASIO_STATIC_CONSTEXPR(uintptr_t, outstanding_work_tracked = 4);
    BOOST_ASIO_STATIC_CONSTEXPR(uintptr_t, runtime_bits = 3);
  };

现在, 似乎被跟踪的执行器具有与 executor_work_guard:

类似的可观察效果
timed_run("No work guard", [] {
    io_context ioc;
    Ex ex = ioc.get_executor();
    ioc.run_for(1s);
});

timed_run("Work guard", [] {
    io_context ioc;
    Ex ex = ioc.get_executor();
    auto w = make_work_guard(ex);
    ioc.run_for(1s);
});

timed_run("Tracked executor", [] {
    io_context ioc;
    auto ex = require(ioc.get_executor(), outstanding_work.tracked);
    
    ioc.run_for(1s);
});

确实,我们可以检查“锁定”执行上下文的可观察 属性 是否传播到副本:

timed_run("Copied tracked executor", [] {
    io_context ioc;

    auto original = std::make_optional(
        require(ioc.get_executor(), outstanding_work.tracked));

    auto copy = *original;

    original.reset();

    ioc.run_for(1s);
});

版画

No work guard: 0ms
Work guard: 1000ms
Tracked executor: 1000ms
Copied tracked executor: 1000ms

然而,实际上是一样的吗?

深入探讨

我不相信它在语义上相同的原因,即使在上述表面确认之后,是论文中的措辞:

突出部分:

The existence of the executor object represents an indication of likely future submission of a function object. The executor or its associated execution context may choose to maintain execution resources in anticipation of this submission.

具体在:

[Note: The outstanding_work_t::tracked_t and outstanding_work_t::untracked_t properties are used to communicate to the associated execution context intended future work submission on the executor. The intended effect of the properties is the behavior of execution context’s facilities for awaiting outstanding work; specifically whether it considers [...] outstanding work when deciding what to wait on. However this will be largely defined by the execution context implementation.

总而言之,我的主要想法是:

  • tracked_t 属性 的遗嘱执行人表明 未来工作的可能性 ,但 不工作
  • 执行上下文的实现定义了该差异是否可观察

Deep-diving 对于我们选择的执行上下文和执行程序类型:

/// Executor implementation type used to submit functions to an io_context.
template <typename Allocator, uintptr_t Bits>
class io_context::basic_executor_type :
  detail::io_context_bits, Allocator
{
public:
  /// Copy constructor.
  basic_executor_type(
      const basic_executor_type& other) BOOST_ASIO_NOEXCEPT
    : Allocator(static_cast<const Allocator&>(other)),
      target_(other.target_)
  {
    if (Bits & outstanding_work_tracked)
      if (context_ptr())
        context_ptr()->impl_.work_started();
  }

正如您在这个特殊情况下所看到的那样,具有 outstanding_work.tracked 的执行器 最终与 executor_work_guard<> 相同的 executor/execution上下文。

SUMMARY/CONCLUSIONS

因此,就所有意图和目的而言,唯一的/正确方法是使用executor_work_guard<>.

但是,对于某些执行上下文和执行程序,效果可能相似。

这也可能是重申 my initial comment executor_work_guard 表达意图的好时机,并且在顶部列出了一些其他优点。