Search code examples
c++boostcrashasio

crash happened when deconstruct boost io_service


I have a problem with boost::asio, when deconstruct a io_service, crash happened, what's wrong in the code?

the backtrace info:

(gdb) bt
#0  boost::asio::detail::scheduler::work_finished (this=this@entry=0x0)
    at /ephemeral/workspace/CB.SBTS_PSINT2.FLOW/build/build/sm6-snowfish-nrt/tmp/work/tremont-64-pc-linux-gnu/sysadapt/67c8d51ea9241fcd4ff1b192870be178f5a70540-r1/recipe-sysroot/usr/include/c++/10.2.0/bits/atomic_base.h:333
#1  0x0000000000785fe6 in boost::asio::io_context::work::~work (this=<synthetic pointer>, __in_chrg=<optimized out>)
    at /ephemeral/workspace/CB.SBTS_PSINT2.FLOW/build/build/sm6-snowfish-nrt/tmp/work/tremont-64-pc-linux-gnu/sysadapt/67c8d51ea9241fcd4ff1b192870be178f5a70540-r1/recipe-sysroot/usr/include/boost/asio/impl/io_context.hpp:427
#2  common::IoServiceThreadGuard::IoServiceThreadGuard(boost::asio::io_context&, unsigned int)::{lambda()#1}::operator()() const (__closure=<optimized out>)

code:

explicit IoServiceThreadGuard(boost::asio::io_service& ioService, unsigned int count) :
        ioService_{ioService},
        threadCount_(count)
{
    for (unsigned int i = 0; i < threadCount_; ++i)
    {
        threads_.create_thread(
                [&]()
                {
                    boost::asio::io_service::work work(ioService_);
                    ioService_.run();
                }); // NOLINT
    }
}
~IoServiceThreadGuard()
{
    try
    {
        if (not ioService_.stopped())
        {
            ioService_.stop();
        }
        threads_.join_all();
    }
    catch (const std::exception& e)
    {
        logger << ::info << "~IoServiceThreadGuard() throw error: " << e.what();
    }
}

Solution

  • As the commenter said, you need to handle exceptions - (Should the exception thrown by boost::asio::io_service::run() be caught?).

    However from looking at the code, the problem seems more likely that you took io_service by reference and it potentially goes out of scope before IoServiceThreadGuard is destructed.

    It seems a lot more natural to give them both the same lifetime:

    Live On Coliru

    #include <boost/asio.hpp>
    #include <boost/thread.hpp>
    #include <iostream>
    namespace asio = boost::asio;
    using namespace std::chrono_literals;
    
    static std::ostream logger(std::clog.rdbuf());
    static constexpr char const* info = "INFO ";
    
    class IoServiceThreadGuard {
      public:
        explicit IoServiceThreadGuard(unsigned count) : threadCount_(count) {
            for (unsigned int i = 0; i < threadCount_; ++i) {
                threads_.create_thread([&]() {
                    asio::io_service::work work(ioService_);
                    ioService_.run();
                });
            }
        }
        ~IoServiceThreadGuard() {
            try {
                if (not ioService_.stopped()) {
                    ioService_.stop();
                }
                threads_.join_all();
            } catch (std::exception const& e) {
                logger << ::info << "~IoServiceThreadGuard() throw error: " << e.what();
            }
        }
    
        asio::io_service& get_service() { return ioService_; }
    
      private:
        asio::io_service    ioService_;
        boost::thread_group threads_; // note that destruction is in reverse order of declaration
        unsigned            threadCount_;
    };
    
    int main() {
        {
            IoServiceThreadGuard io(10);
    
            asio::steady_timer timer(io.get_service(), 1s);
            timer.async_wait([](auto ec) { logger << ::info << "timer " << ec.message() << "\n"; });
    
            io.get_service().run_for(2s);
        }
    
        logger << "Done" << std::endl;
    }
    

    Even better, use the non-deprecated io_context: Live

    Even better, drop the Boost Thread dependency and use asio::thread_pool directly.

    Now it all works in just 5 lines of code, and it does the correct thing with exception handling as well!

    Live On Coliru

    #include <boost/asio.hpp>
    #include <boost/thread.hpp>
    #include <iostream>
    namespace asio = boost::asio;
    using namespace std::chrono_literals;
    
    static std::ostream logger(std::clog.rdbuf());
    static constexpr char const* info = "INFO ";
    
    struct IoThreads {
        explicit IoThreads(unsigned count) : ioc_(count) {}
        asio::thread_pool& get_context() { return ioc_; }
      private:
        asio::thread_pool ioc_;
    };
    
    int main() {
        for (auto time_allotted : {0.5s, 2.0s}) {
            logger << "Using 4 threads for " << time_allotted / 1.s << "s" << std::endl;
            IoThreads io(4);
    
            asio::steady_timer timer(io.get_context(), 1s);
            timer.async_wait([](auto ec) { logger << ::info << "timer " << ec.message() << "\n"; });
    
            std::this_thread::sleep_for(time_allotted);
        }
    
        logger << "Done" << std::endl;
    }
    

    Prints (in total run time of 2.518s):

    Using 4 threads for 0.5s
    Using 4 threads for 2s
    INFO timer Success
    Done
    

    Simplify, Generalize, De-couple

    At this point, consider forgetting the redundant "guard" class, and also consider passing an executor (by value) instead of hardcoding asio::io_service&, asio::io_context& or asio::thread_pool&. The executor is a lightweight abstraction that decouples your asynchronous code from the execution context. For example, you might use a strand-executor in multi-threaded context, without the async code needing to know.

    Live On Coliru

    #include <boost/asio.hpp>
    #include <boost/thread.hpp>
    #include <iostream>
    namespace asio = boost::asio;
    using boost::core::demangle;
    using namespace std::chrono_literals;
    
    class Demo { // just a sample, this could be a network server/client
      public:
        Demo(asio::any_io_executor ex) : timer_(ex) {} // take an executor, don't care what type
    
        void run_async_stuff() {
            std::cout << "Demo on " << demangle(timer_.get_executor().target_type().name()) << std::endl;
    
            timer_.expires_after(1s);
            timer_.async_wait(on_completion); // or something like `asio::async_read` etc.
        }
    
      private:
        static void on_completion(boost::system::error_code ec) {
            std::cout << "async_stuff " << ec.message() << "\n";
        }
    
        asio::steady_timer timer_; // this could be tcp::socket or so
    };
    
    int main() {
        {
            asio::io_context io(1);
            {
                Demo demo(io.get_executor()); // no strand required
                demo.run_async_stuff();
    
                io.run_for(500ms);
            }         // ~Demo cancels uncompleted async operation
            io.run(); // To see `operaion_aborted` completion
        }
    
        {
            asio::thread_pool io(10);
            Demo demo(make_strand(io)); // notice strand executor
            demo.run_async_stuff();
    
            std::this_thread::sleep_for(1.5s);
        }
    
        std::cout << "Done" << std::endl;
    }
    

    Prints e.g.

    Demo on boost::asio::io_context::basic_executor_type<std::allocator<void>, 0ul>
    async_stuff Operation canceled
    Demo on boost::asio::strand<boost::asio::thread_pool::basic_executor_type<std::allocator<void>, 0u> >
    async_stuff Success
    Done