安全取消 boost asio 截止时间定时器

17

我正在尝试安全地取消一个boost::asio::basic_waitable_timer<std::chrono::steady_clock>

根据这个答案,这段代码应该可以完成此工作:

timer.get_io_service().post([&]{timer.cancel();})

很抱歉,它对我不起作用。
我做错了什么吗?
这是我的代码:

#include <iostream>
#include "boost/asio.hpp"
#include <chrono>
#include <thread>
#include <random>

boost::asio::io_service io_service;
boost::asio::basic_waitable_timer<std::chrono::steady_clock> timer(io_service);
std::atomic<bool> started;

void handle_timeout(const boost::system::error_code& ec)
{
    if (!ec) {
        started = true;
        std::cerr << "tid: " << std::this_thread::get_id() << ", handle_timeout\n";
        timer.expires_from_now(std::chrono::milliseconds(10));
        timer.async_wait(&handle_timeout);
    } else if (ec == boost::asio::error::operation_aborted) {
        std::cerr << "tid: " << std::this_thread::get_id() << ", handle_timeout aborted\n";
    } else {
        std::cerr << "tid: " << std::this_thread::get_id() << ", handle_timeout another error\n";
    }
}

int main() {

    std::cout << "tid: " << std::this_thread::get_id() << ", Hello, World!" << std::endl;
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<> dis(1, 100);

    for (auto i = 0; i < 1000; i++) {

        started = false;
        std::thread t([&](){

            timer.expires_from_now(std::chrono::milliseconds(0));
            timer.async_wait(&handle_timeout);

            io_service.run();
        });

        while (!started) {};
        auto sleep = dis(gen);
        std::cout << "tid: " << std::this_thread::get_id() << ", i: " << i << ", sleeps for " << sleep << " [ms]" << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(sleep));
        timer.get_io_service().post([](){
            std::cerr << "tid: " << std::this_thread::get_id() << ", cancelling in post\n";
            timer.cancel();
        });
//      timer.cancel();
        std::cout << "tid: " << std::this_thread::get_id() << ", i: " << i << ", waiting for thread to join()" << std::endl;
        t.join();
        io_service.reset();
    }

    return 0;
}

这是输出结果:

...
tid: 140737335076608, 处理超时
tid: 140737335076608, 处理超时
tid: 140737353967488, i: 2, 等待线程加入()
tid: 140737335076608, 在post中取消
tid: 140737335076608, 处理超时中止
tid: 140737353967488, i: 3, 睡眠21 [ms]
tid: 140737335076608, 处理超时
tid: 140737353967488, i: 3, 等待线程加入()
tid: 140737335076608, 处理超时
tid: 140737335076608, 在post中取消
tid: 140737335076608, 处理超时
tid: 140737335076608, 处理超时
tid: 140737335076608, 处理超时
tid: 140737335076608, 处理超时
tid: 140737335076608, 处理超时
...
永远继续...

如您所见,timer.cancel()被从适当的线程调用:

tid: 140737335076608, 在post中取消

但之后没有

tid: 140737335076608, 处理超时中止

之后,主线程永远等待。
1个回答

31

取消操作是安全的。

只是不够健壮。您没有考虑计时器未处于挂起状态的情况。您只取消了一次,但是一旦完成处理程序被调用,它将重新开始异步等待。

接下来是我追踪问题的详细步骤。

摘要 TL;DR

仅取消时间会取消正在进行的异步操作。

如果您想关闭异步调用链,您需要使用其他逻辑。下面给出了一个示例。

处理程序跟踪

启用方式为

#define BOOST_ASIO_ENABLE_HANDLER_TRACKING 1

这将产生输出,可以使用boost/libs/asio/tools/handlerviz.pl进行可视化:

成功的跟踪

enter image description here

正文:如你所见,当取消发生时,async_wait处于执行状态。

一条“坏”的跟踪信息

正文:(已截断,否则将会无限运行)

enter image description here

请注意完成处理程序看到的是cc=system:0,而不是cc=system:125(对于operation_aborted)。这表明发布的取消请求实际上并没有“生效”。唯一的逻辑解释(在图表中不可见)是定时器在调用取消请求之前已经过期。
让我们比较原始跟踪¹。

enter image description here

¹去除噪音的差异

检测它

那么,我们有了线索。我们能够检测到它吗?

    timer.get_io_service().post([](){
        std::cerr << "tid: " << std::this_thread::get_id() << ", cancelling in post\n";
        if (timer.expires_from_now() >= std::chrono::steady_clock::duration(0)) {
            timer.cancel();
        } else {
            std::cout << "PANIC\n";
            timer.cancel();
        }
    });

输出:

tid: 140113177143232, i: 0, waiting for thread to join()
tid: 140113177143232, i: 1, waiting for thread to join()
tid: 140113177143232, i: 2, waiting for thread to join()
tid: 140113177143232, i: 3, waiting for thread to join()
tid: 140113177143232, i: 4, waiting for thread to join()
tid: 140113177143232, i: 5, waiting for thread to join()
tid: 140113177143232, i: 6, waiting for thread to join()
tid: 140113177143232, i: 7, waiting for thread to join()
tid: 140113177143232, i: 8, waiting for thread to join()
tid: 140113177143232, i: 9, waiting for thread to join()
tid: 140113177143232, i: 10, waiting for thread to join()
tid: 140113177143232, i: 11, waiting for thread to join()
tid: 140113177143232, i: 12, waiting for thread to join()
tid: 140113177143232, i: 13, waiting for thread to join()
tid: 140113177143232, i: 14, waiting for thread to join()
tid: 140113177143232, i: 15, waiting for thread to join()
tid: 140113177143232, i: 16, waiting for thread to join()
tid: 140113177143232, i: 17, waiting for thread to join()
tid: 140113177143232, i: 18, waiting for thread to join()
tid: 140113177143232, i: 19, waiting for thread to join()
tid: 140113177143232, i: 20, waiting for thread to join()
tid: 140113177143232, i: 21, waiting for thread to join()
tid: 140113177143232, i: 22, waiting for thread to join()
tid: 140113177143232, i: 23, waiting for thread to join()
tid: 140113177143232, i: 24, waiting for thread to join()
tid: 140113177143232, i: 25, waiting for thread to join()
tid: 140113177143232, i: 26, waiting for thread to join()
PANIC

我们能用另一种更清晰的方式来传达“超级取消”吗?当然,我们只有timer对象可用:

信号关闭

timer对象没有太多可用的属性。没有像套接字上的close()或类似的东西,可以用来将计时器置于某种无效状态。

但是,有到期时间点,我们可以使用特殊的域值来表示我们的应用程序的“无效”信号:

timer.get_io_service().post([](){
    std::cerr << "tid: " << std::this_thread::get_id() << ", cancelling in post\n";
    // also cancels:
    timer.expires_at(Timer::clock_type::time_point::min());
});

这个“特殊值”在完成处理程序中很容易处理:

void handle_timeout(const boost::system::error_code& ec)
{
    if (!ec) {
        started = true;
        if (timer.expires_at() != Timer::time_point::min()) {
            timer.expires_from_now(std::chrono::milliseconds(10));
            timer.async_wait(&handle_timeout);
        } else {
            std::cerr << "handle_timeout: detected shutdown\n";
        }
    } 
    else if (ec != boost::asio::error::operation_aborted) {
        std::cerr << "tid: " << std::this_thread::get_id() << ", handle_timeout error " << ec.message() << "\n";
    }
}

2
不错的解决方法,但是...你觉得应该有更好的取消功能来隐藏所有这些实现细节的混乱吗?与取消相关的问题一次又一次地出现... - Igor R.
2
@hudac 我只是确认你使用它是线程安全的,我并没有说其他任何事情。你之所以可以安全地使用它,是因为你将其发布到服务中,并且该服务在单个线程上运行,这意味着你获得了“隐式串行”行为(没有两个处理程序会同时运行)。 - sehe
3
更具体地说,一旦您在更多的线程上运行服务,那么这不再是一个经验法则!在这种情况下,您需要一个strand来同步访问服务对象(比如deadline_timer)。请参阅https://dev59.com/ZGcs5IYBdhLWcg3wgkMQ#12801042。希望这能够明确表达`cancel()`不是线程安全的事实,并且这已在文档中说明(没有人会否认这一点)。 - sehe
@sehe,你有没有类似这样的解决方案可以安全地取消boost::asio::signal_set?或者我应该使用一些“shutdown”标志? - hudac
1
@hudac 我觉得我不需要(通常只需监听一次INT/TERM)。当然,你可以简单地使用 signal_set.clear(...); (现在当你收到信号0时,这意味着你应该关闭)。 - sehe
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接