首页 > 解决方案 > ASIO 计时器 `cancel()` 可以调用虚假的“成功”吗?

问题描述

ASIO 文档basic_deadline_timer::cancel()有以下备注部分:

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

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

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

重点是我加的。通常,当您调用cancel()计时器时,会运行回调并显示“用户取消操作”的错误代码。但这表明它实际上可以用成功错误代码调用的可能性很小。我认为它试图说可能会发生以下情况:

  1. 线程 A 调用async_wait(myTimerHandler)计时器,其中myTimerHandler()是用户回调函数。
  2. 线程 B 调用io_context::post(cancelMyTimer)wherecancelMyTimer()是一个用户回调函数。现在排队等待在线程 A 中调用。
  3. 定时器期限到期,因此 ASIO 将定时器回调处理程序排队,并带有成功错误代码。它还没有调用它,但它已排队等待在线程 A 中调用。
  4. ASIO 开始调用cancelMyTimer()线程 A,它调用cancel()计时器。但是计时器已经触发,并且 ASIO 不会检查处理程序是否仍在排队并且没有执行,所以这什么都不做。
  5. ASIO 现在调用myTimerHandler,同时不检查是否cancel()被调用,因此它仍然将成功作为错误代码传递。

记住这个例子只有一个线程调用io_context::run()deadline_timer::async_wait或者deadline_timer::cancel()。在另一个线程中发生的唯一事情是对 的调用post(),这是为了避免任何竞争条件而发生的。这一系列事件可能吗?或者它是指一些多线程场景(鉴于计时器不是线程安全的,这似乎不太可能)?

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

标签: timerboost-asio

解决方案


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

更新:

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

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

Boost 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;
}

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

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

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


推荐阅读