ASIO计时器`cancel()`可以调用虚假的“成功”吗?

时间:2019-03-16 18:42:58

标签: timer boost-asio

ASIO documentation for basic_deadline_timer::cancel()具有以下备注部分:

  

如果在调用cancel()时计时器已经到期,则用于异步等待操作的处理程序将:

     
      
  • 已经被调用;或
  •   
  • 已经在不久的将来排队等待调用。
  •   
     

这些处理程序无法再被取消,因此 传递了一个错误代码,指示成功完成了等待操作

重点已由我添加。通常,当您在计时器上调用cancel()时,将以错误代码“用户取消操作”运行回调。但是,这实际上很少会用成功错误代码来调用它。我认为这是想说可能发生以下情况:

  1. 线程A在计时器上调用async_wait(myTimerHandler),其中myTimerHandler()是用户回调函数。
  2. 线程B调用io_context::post(cancelMyTimer),其中cancelMyTimer()是用户回调函数。现在已排队等待在线程A中调用。
  3. 计时器最后期限到期,因此ASIO将计时器回调处理程序排队,并带有成功错误代码。它尚未调用,但已排队等待在线程A中调用。
  4. ASIO开始在线程A中调用cancelMyTimer(),该线程在计时器上调用cancel()。但是计时器已经触发,并且ASIO不会检查处理程序是否仍在排队并且没有执行,因此此操作无济于事。
  5. ASIO现在调用myTimerHandler,并且不会检查在此期间是否cancel()被调用,因此它仍然成功作为错误代码传递。

请记住,此示例仅具有一个调用io_context::run()deadline_timer::async_waitdeadline_timer::cancel()的线程。在另一个线程中发生的唯一事情是对post()的调用,该调用是为了避免出现任何竞争条件而发生的。这一系列事件可能吗?还是它是指某种多线程方案(考虑到计时器不是线程安全的,这似乎不太可能)?

上下文:如果您有一个希望定期重复的计时器,那么显而易见的事情是检查回调中的错误代码,如果代码成功,则再次设置计时器。如果可以进行上述比赛,则必须有一个单独的变量来说明是否取消了计时器,除了调用cancel()以外,还需要更新该计时器。

2 个答案:

答案 0 :(得分:1)

您所说的一切都是正确的。因此,根据您的情况,您可能需要一个单独的变量来表示您不想继续循环。我通常使用atomic_bool,并且不必费心发布取消例程,只需在我所处的任何线程中设置bool&call cancel。

更新:

我的答案的来源主要是多年使用ASIO的经验以及对asio代码库的了解足以解决问题并在需要时扩展它的一部分。

是的,该文档说在截止截止时间的共享实例之间不是线程安全的,但是该文档不是最好的(什么文档是...)。如果您查看“取消”工作原理的来源,我们可以看到:

提升Asio版本1.69:boost \ asio \ detail \ impl \ win_iocp_io_context.hpp

template <typename Time_Traits>
std::size_t win_iocp_io_context::cancel_timer(timer_queue<Time_Traits>& queue,
    typename timer_queue<Time_Traits>::per_timer_data& timer,
    std::size_t max_cancelled)
{
  // If the service has been shut down we silently ignore the cancellation.
  if (::InterlockedExchangeAdd(&shutdown_, 0) != 0)
    return 0;

  mutex::scoped_lock lock(dispatch_mutex_);
  op_queue<win_iocp_operation> ops;
  std::size_t n = queue.cancel_timer(timer, ops, max_cancelled);
  post_deferred_completions(ops);
  return n;
}

您可以看到取消操作由互斥锁保护,因此“取消”操作是线程安全的。

不是在截止时间计时器上调用大多数其他操作(关于从多个线程同时调用它们)。

我也认为您对以快速顺序重新启动计时器是正确的。我通常没有用这种方式停止和启动计时器的用例,所以我从来不需要这样做。

答案 1 :(得分:1)

您甚至不需要第二个线程就可以进入basic_waitable_timer::cancel()调用太晚的情况(因为计时器的(完成)处理程序已经排队)。

您的程序要与尚未恢复的basic_waitable_timer::async_wait()同时执行一些其他异步操作就足够了。如果您然后仅依靠basic_waitable_timer::cancel()进行取消,则来自另一个异步(完成)处理程序的cancel()调用将与已调度的async_wait()处理程序竞争:

如果在调用cancel()时计时器已经过期,则用于异步等待操作的处理程序将:

  • 已经被调用;或
  • 已在不久的将来排队等待调用。

这些处理程序无法再被取消,因此会传递一个错误代码,指示成功完成等待操作。

basic_waitable_timer::cancel(),重点是我,即比赛情况是由于第二种情况引起的)

一个单线程的现实示例(即程序没有显式启动任何线程,仅调用io_server.run()一次)并包含描述的种族:

    void Fetch_Timer::resume()
    {
      timer_.expires_from_now(std::chrono::seconds(1));
      timer_.async_wait([this](const boost::system::error_code &ec)
          {
            BOOST_LOG_FUNCTION();
            if (ec) {
              if (ec.value() == boost::asio::error::operation_aborted)
                return;
              THROW_ERROR(ec);
            } else {
              print();
              resume();
            }
          });
    }
    void Fetch_Timer::stop()
    {
      print();
      timer_.cancel();
    }

(来源:imapdl/copy/fetch_timer.cc

在此示例中,obvious fix(即也查询布尔标志)甚至不需要使用任何同步原语(例如原子),因为该程序是单线程的。这意味着它可以并发执行(异步)操作,但不能并行执行。

(在上面的示例中,FWIW,该漏洞仅每2年左右就出现一次,即使每天使用也是如此)