【问题标题】:Stopping boost::asio::io_service::run() from concurrent destructor从并发析构函数中停止 boost::asio::io_service::run()
【发布时间】:2016-05-08 23:30:35
【问题描述】:

谁能解释一下为什么这个程序没有终止(见 cmets)?

#include <boost/asio/io_service.hpp>
#include <boost/asio.hpp>
#include <memory>
#include <cstdio>
#include <iostream>
#include <future>

class Service {
public:
    ~Service() {
        std::cout << "Destroying...\n";
        io_service.post([this]() {
            std::cout << "clean and stop\n"; // does not get called
            // do some cleanup
            // ...
            io_service.stop();
            std::cout << "Bye!\n";
        });
        std::cout << "...destroyed\n"; // last printed line, blocks
    }

    void operator()() {
        io_service.run();
        std::cout << "run completed\n";
    }

private:
    boost::asio::io_service io_service;
    boost::asio::io_service::work work{io_service};
};

struct Test {
    void start() {
        f = std::async(std::launch::async, [this]() { service(); std::cout << "exiting thread\n";});
    }
    std::future<void> f;
    Service service;
};

int main(int argc, char* argv[]) {
    {
        Test test;
        test.start();

        std::string exit;
        std::cin >> exit;
    }

    std::cout << "exiting program\n"; // never printed
}

【问题讨论】:

  • 如果三个小时还不够快,也许你应该雇人:)

标签: c++ boost boost-asio


【解决方案1】:

真正的问题是io_service 的销毁(显然)不是线程安全的。

只需重置工作并加入线程。或者,设置一个标志,以便您的 IO 操作知道正在关闭。

您的测试和服务类试图分担 IO 服务的责任,这是行不通的。这里简化了很多,合并了类并删除了未使用的未来。

Live On Coliru

诀窍是让work 对象optional&lt;&gt;

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

struct Service {
    ~Service() {
        std::cout << "clean and stop\n";
        io_service.post([this]() {
            work.reset(); // let io_service run out of work
        });

        if (worker.joinable())
            worker.join();
    }

    void start() {
        assert(!worker.joinable());
        worker = std::thread([this] { io_service.run(); std::cout << "exiting thread\n";});
    }

private:
    boost::asio::io_service io_service;
    std::thread worker;
    boost::optional<boost::asio::io_service::work> work{io_service};
};

int main() {
    {
        Service test;
        test.start();

        std::cin.ignore(1024, '\n');
        std::cout << "Start shutdown\n";
    }

    std::cout << "exiting program\n"; // never printed
}

打印

Start shutdown
clean and stop
exiting thread
exiting program

【讨论】:

  • 如果我没记错的话,我确实尝试过重置工人(在我的情况下它是一个 unique_ptr)。尽管从真实案例中提取的这个简单示例似乎没有用,但我需要的是在销毁工人之前真正调用我的问题中所写的 stop() 。稍后我会再试一次,但如果我没记错的话它会一直崩溃。
  • @Martin 不要“记住”。只需查看问题中的代码即可。工作重置不存在(另外,不要混淆workworker)。我的代码不会崩溃。希望对您有所帮助。
  • 太棒了!这是我在 SO 上找到的唯一答案,它实际上阻止了我的 io_service.run()
【解决方案2】:

请看这里:boost::asio hangs in resolver service destructor after throwing out of io_service::run()

我认为这里的技巧是在调用io_service.stop() 之前销毁worker(work 成员)。 IE。在这种情况下,work 可以是unique_ptr,并在停止服务之前显式调用reset()

编辑:在我的情况下,上述内容帮助了我一段时间,ioservice::stop 并没有停止,而是在等待一些从未发生过的调度事件。

但是,我在我的机器上重现了您遇到的问题,这似乎是 ioservice 内部的竞争条件,ioservice::post()ioservice 破坏代码 (shutdown_service) 之间的竞争。特别是,如果在post() 通知唤醒另一个线程之前触发了shutdown_service()shutdown_service() 代码会从队列中删除操作(并“销毁”它而不是调用它),因此 lambda 永远不会然后调用。

目前在我看来,您需要直接在析构函数中调用 io_service.stop(),而不是通过 post() 推迟,因为由于比赛,这显然在这里不起作用。

【讨论】:

  • 调用io_service.stop()之前是什么意思?在兰巴车内还是车外? inside 不起作用,因为 lambda 甚至没有被调用
  • 查看更新,post() 和销毁 (shutdown_service()) 之间似乎存在竞争条件。
  • 但在清理后推迟它是我的真正意图..所以这是 boost::asio 中的错误吗?从文档中,我希望在销毁工作时调用所有待处理的处理程序
  • 我不太确定,我也没有测试最新的 Boost,但对我来说,似乎所有待处理的处理程序仅在调用 stop() 时才执行;但是如果服务在没有首先调用stop() 的情况下被销毁,shutdown_service() 会清理未运行处理程序的队列(即取消它们)。因此,如果其他线程之前没有设法运行所有处理程序,那么它们将永远不会被执行。
【解决方案3】:

我能够通过像这样重写您的代码来解决问题:

class Service {
public:
    ~Service() {
        std::cout << "Destroying...\n";
        work.reset();
        std::cout << "...destroyed\n"; // last printed line, blocks
    }

    void operator()() {
        io_service.run();
        std::cout << "run completed\n";
    }

private:
    boost::asio::io_service io_service;
    std::unique_ptr<boost::asio::io_service::work> work = std::make_unique<boost::asio::io_service::work>(io_service);
};

但是,这在很大程度上是一种创可贴的解决方案。

问题在于您的设计理念;具体来说,选择不将执行线程的生命周期直接绑定到io_service 对象:

struct Test {
    void start() {
        f = std::async(std::launch::async, [this]() { service(); std::cout << "exiting thread\n";});
    }
    std::future<void> f; //Constructed First, deleted last
    Service service; //Constructed second, deleted first
};

在这种特殊情况下,线程将继续尝试在 io_service 对象本身的生命周期之后执行 io_service.run()。如果在服务上执行的不仅仅是基本的work 对象,您很快就会开始通过调用已删除对象的成员函数来处理未定义的行为。

你可以颠倒Test中成员对象的顺序:

struct Test {
    void start() {
        f = std::async(std::launch::async, [this]() { service(); std::cout << "exiting thread\n";});
    }
    Service service;
    std::future<void> f;
};

但它仍然代表着一个重大的设计缺陷。

我通常实现任何使用io_service 的方法是将其生命周期与实际将在其上执行的线程联系起来。

class Service {
public:
    Service(size_t num_of_threads = 1) :
        work(std::make_unique<boost::asio::io_service::work>(io_service))
    {
        for (size_t thread_index = 0; thread_index < num_of_threads; thread_index++) {
            threads.emplace_back([this] {io_service.run(); });
        }
    }

    ~Service() {
        work.reset();
        for (std::thread & thread : threads) 
            thread.join();
    }
private:
    boost::asio::io_service io_service;
    std::unique_ptr<boost::asio::io_service::work> work;
    std::vector<std::thread> threads;
};

现在,如果您在这些线程中的任何一个上激活了无限循环,您仍然需要确保正确清理它们,但至少该io_service 操作的特定代码已正确清理.

【讨论】:

  • 为什么要引入更多的工作线程?这可能完全不适合 OP
  • “如果在服务上执行的不仅仅是基本工作对象,您很快就会开始处理未定义的行为” 具有误导性。无论如何都是UB
  • 我实际上喜欢您的建设性建议 - 使用线程容器和所有内容,但答案的第一部分(“创可贴”)确实需要删除。这不是创可贴,也无助于解释问题。
猜你喜欢
  • 2011-06-16
  • 2011-03-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多