0

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();
    }
}
sehe
  • 374,641
  • 47
  • 450
  • 633
merkey
  • 5
  • 2
  • `ioService_.run();` may thrown an exception. You don't catch it. Exception cannot be thrown across threads, try/catch in `IoServiceThreadGuard` destructor is not enough. You have to catch this exception inside body of thread or find another way to [propagate it](https://stackoverflow.com/questions/233127/how-can-i-propagate-exceptions-between-threads). – rafix07 Apr 13 '23 at 07:00

1 Answers1

0

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
sehe
  • 374,641
  • 47
  • 450
  • 633