ASIO 计时器 `cancel()` 可以调用虚假的 "success" 吗?
Can ASIO timer `cancel()` call a spurious "success"?
ASIO documentation for basic_deadline_timer::cancel()
有如下备注部分:
If the timer has already expired when cancel()
is called, then the handlers for asynchronous wait operations will:
- have already been invoked; or
- have been queued for invocation in the near future.
These handlers can no longer be cancelled, and therefore are passed an error code that indicates the successful completion of the wait operation.
重点是我加的。通常,当您在计时器上调用 cancel()
时,回调是 运行,错误代码为 "operation cancelled by user"。但这表示实际上可以使用成功错误代码调用它的可能性很小。我认为它试图说明以下情况可能发生:
- 线程 A 在计时器上调用
async_wait(myTimerHandler)
,其中 myTimerHandler()
是用户回调函数。
- 线程 B 调用
io_context::post(cancelMyTimer)
,其中 cancelMyTimer()
是用户回调函数。现在排队等待在线程 A 中调用。
- 计时器截止时间到期,因此 ASIO 将计时器回调处理程序排队,并显示成功错误代码。它还没有调用它,但它正在排队等待在线程 A 中调用。
- ASIO 转而在线程 A 中调用
cancelMyTimer()
,线程 A 在计时器上调用 cancel()
。但是计时器已经触发,ASIO 不检查处理程序是否仍在排队且未执行,所以这什么都不做。
- ASIO 现在调用
myTimerHandler
,并且不检查同时调用了 cancel()
,因此它仍然传递成功作为错误代码。
请记住,此示例只有一个线程调用 io_context::run()
、deadline_timer::async_wait
或 deadline_timer::cancel()
。另一个线程中唯一发生的事情是对 post()
的调用,这是为了避免任何竞争条件而发生的。这一系列事件可能吗?或者它指的是一些多线程场景(考虑到计时器不是线程安全的,这似乎不太可能)?
上下文:如果你有一个你希望定期重复的计时器,那么显而易见的事情就是检查回调中的错误代码,如果代码成功则再次设置计时器。如果上述比赛是可能的,那么有必要有一个单独的变量来说明你是否取消了计时器,除了调用 cancel()
之外你还更新它。
你所说的一切都是正确的。因此,在您的情况下,您可能需要一个单独的变量来指示您不想继续循环。我通常使用 atomic_bool 并且我不费心发布取消例程,我只是设置 bool 并从我所在的任何线程调用取消。
更新:
我的回答主要来自多年来使用 ASIO 的经验,以及对 asio 代码库的充分理解,以解决问题并在需要时扩展其中的一部分。
是的,文档说 deadline_timer 的共享实例之间不是线程安全的,但文档不是最好的(什么文档是...)。如果您查看 "cancel" 工作原理的来源,我们可以看到:
提升 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;
}
您可以看到取消操作由互斥锁保护,因此 "cancel" 操作是线程安全的。
在截止日期计时器上调用大多数其他操作不是(关于从多个线程同时调用它们)。
此外,我认为您关于按快速顺序重新启动计时器的说法是正确的。我通常没有以那种方式停止和启动计时器的用例,所以我从来不需要这样做。
您甚至不需要第二个线程来 运行 进入 basic_waitable_timer::cancel()
调用太晚的情况(因为计时器的(完成)处理程序已经排队)。
您的程序对尚未恢复的basic_waitable_timer::async_wait()
并发执行一些其他异步操作就足够了。如果您随后仅依赖 basic_waitable_timer::cancel()
进行取消,那么来自另一个异步(完成)处理程序的 cancel()
调用将与已安排的 async_wait()
处理程序竞争:
If the timer has already expired when cancel() is called, then the handlers for asynchronous wait operations will:
- have already been invoked; or
- have been queued for invocation in the near future.
These handlers can no longer be cancelled, and therefore are passed an error code that indicates the successful completion of the wait operation.
(basic_waitable_timer::cancel(),强调我的,即竞争条件是由于第二种情况)
一个 real-world 示例,它是 single-threaded(即程序没有明确启动任何线程,只调用 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(也就是查询布尔标志)甚至不需要使用任何同步原语(例如原子),因为程序是single-threaded。这意味着它同时执行(异步)操作但不是并行执行。
(FWIW,在上面的示例中,即使在日常使用下,该错误也仅每 2 年左右出现一次)
ASIO documentation for basic_deadline_timer::cancel()
有如下备注部分:
If the timer has already expired when
cancel()
is called, then the handlers for asynchronous wait operations will:
- have already been invoked; or
- have been queued for invocation in the near future.
These handlers can no longer be cancelled, and therefore are passed an error code that indicates the successful completion of the wait operation.
重点是我加的。通常,当您在计时器上调用 cancel()
时,回调是 运行,错误代码为 "operation cancelled by user"。但这表示实际上可以使用成功错误代码调用它的可能性很小。我认为它试图说明以下情况可能发生:
- 线程 A 在计时器上调用
async_wait(myTimerHandler)
,其中myTimerHandler()
是用户回调函数。 - 线程 B 调用
io_context::post(cancelMyTimer)
,其中cancelMyTimer()
是用户回调函数。现在排队等待在线程 A 中调用。 - 计时器截止时间到期,因此 ASIO 将计时器回调处理程序排队,并显示成功错误代码。它还没有调用它,但它正在排队等待在线程 A 中调用。
- ASIO 转而在线程 A 中调用
cancelMyTimer()
,线程 A 在计时器上调用cancel()
。但是计时器已经触发,ASIO 不检查处理程序是否仍在排队且未执行,所以这什么都不做。 - ASIO 现在调用
myTimerHandler
,并且不检查同时调用了cancel()
,因此它仍然传递成功作为错误代码。
请记住,此示例只有一个线程调用 io_context::run()
、deadline_timer::async_wait
或 deadline_timer::cancel()
。另一个线程中唯一发生的事情是对 post()
的调用,这是为了避免任何竞争条件而发生的。这一系列事件可能吗?或者它指的是一些多线程场景(考虑到计时器不是线程安全的,这似乎不太可能)?
上下文:如果你有一个你希望定期重复的计时器,那么显而易见的事情就是检查回调中的错误代码,如果代码成功则再次设置计时器。如果上述比赛是可能的,那么有必要有一个单独的变量来说明你是否取消了计时器,除了调用 cancel()
之外你还更新它。
你所说的一切都是正确的。因此,在您的情况下,您可能需要一个单独的变量来指示您不想继续循环。我通常使用 atomic_bool 并且我不费心发布取消例程,我只是设置 bool 并从我所在的任何线程调用取消。
更新:
我的回答主要来自多年来使用 ASIO 的经验,以及对 asio 代码库的充分理解,以解决问题并在需要时扩展其中的一部分。
是的,文档说 deadline_timer 的共享实例之间不是线程安全的,但文档不是最好的(什么文档是...)。如果您查看 "cancel" 工作原理的来源,我们可以看到:
提升 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;
}
您可以看到取消操作由互斥锁保护,因此 "cancel" 操作是线程安全的。
在截止日期计时器上调用大多数其他操作不是(关于从多个线程同时调用它们)。
此外,我认为您关于按快速顺序重新启动计时器的说法是正确的。我通常没有以那种方式停止和启动计时器的用例,所以我从来不需要这样做。
您甚至不需要第二个线程来 运行 进入 basic_waitable_timer::cancel()
调用太晚的情况(因为计时器的(完成)处理程序已经排队)。
您的程序对尚未恢复的basic_waitable_timer::async_wait()
并发执行一些其他异步操作就足够了。如果您随后仅依赖 basic_waitable_timer::cancel()
进行取消,那么来自另一个异步(完成)处理程序的 cancel()
调用将与已安排的 async_wait()
处理程序竞争:
If the timer has already expired when cancel() is called, then the handlers for asynchronous wait operations will:
- have already been invoked; or
- have been queued for invocation in the near future.
These handlers can no longer be cancelled, and therefore are passed an error code that indicates the successful completion of the wait operation.
(basic_waitable_timer::cancel(),强调我的,即竞争条件是由于第二种情况)
一个 real-world 示例,它是 single-threaded(即程序没有明确启动任何线程,只调用 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(也就是查询布尔标志)甚至不需要使用任何同步原语(例如原子),因为程序是single-threaded。这意味着它同时执行(异步)操作但不是并行执行。
(FWIW,在上面的示例中,即使在日常使用下,该错误也仅每 2 年左右出现一次)