Colin Ray
Colin Ray

Reputation: 175

Understanding a memory leak while using boost::asio and boost::thread

I am working on a set of classes that make use of boost::asio to perform background tasks. In practice, the program will run continuously, but I have added signal handlers for cleanup during testing.

However, when monitoring function calls in the code after SIGINT is received, I discovered that the private implementation of my object is not being destroyed as expected - a memory leak. It is managed with a boost::shared_ptr. The private implementation class is shown below.

class TestImpl: public boost::enable_shared_from_this<TestImpl>, boost::noncopyable {
    TestImpl(): update_timer(io_svc), signals(io_svc, SIGINT, SIGTERM) {
        signals.async_wait(boost::bind(&boost::asio::io_service::stop, &io_svc)); 
    };

public:
    virtual ~TestImpl() { 
        std::cout << "Destroyed." << std::endl;
    };

    static boost::shared_ptr<TestImpl> create() {
        boost::shared_ptr<TestImpl> ptr(new TestImpl);
        ptr->start();
        return ptr;
    }

    void start() {
        update_timer.expires_from_now(boost::posix_time::seconds(1));
        update_timer.async_wait(boost::bind(&TestImpl::update, shared_from_this()));    

        run_thread = boost::thread(boost::bind(&TestImpl::run, shared_from_this()));
    };

    void cleanup() {
        run_thread.join();
    };

private:
    void run() {
        io_svc.run();
    };

    void update() {
        std::cout << "Updating." << std::endl;
        update_timer.expires_from_now(boost::posix_time::seconds(1));
        update_timer.async_wait(boost::bind(&TestImpl::update, shared_from_this()));    
    };

    boost::asio::io_service io_svc;
    boost::asio::deadline_timer update_timer;
    boost::thread run_thread;
    boost::asio::signal_set signals;
};

Here is the code that is using the private implementation.

class Test {
public:
    Test(): impl(TestImpl::create()) { };
    virtual ~Test() { std::cout << "Destroyed." << std::endl; };
    int run() {
        boost::asio::signal_set signals(io_svc, SIGINT, SIGTERM);
        signals.async_wait(boost::bind(&boost::asio::io_service::stop, &io_svc));

        io_svc.run();

        impl->cleanup();

        return 0;
    };
private:
    boost::asio::io_service io_svc;
    boost::shared_ptr<TestImpl> impl;
};

int main() {
    Test test;
    test.run();
}

I am having trouble understanding why the TestImpl class gets leaked. By debugging I can verify that both io_service instances are stopped upon SIGINT and that the thread gets joined, which leads me to believe that it does not get detached upon destruction. It seems like there must be a circular reference somewhere that is causing the TestImpl instance to persist?

Upvotes: 4

Views: 3032

Answers (1)

Tanner Sansbury
Tanner Sansbury

Reputation: 51871

The circular reference is between TestImpl and TestImpl::io_svc:

  • TestImpl::io_svc's lifespan depends on TestImpl as it is a member variable.
  • TestImpl's lifespan indirectly depends on TestIMpl::io_svc due to shared_from_this() being bound as the instance handle within handlers queued within the io_service.

A critical detail is that io_service::stop() only affects the event processing loop; it does not affect the lifespan of handlers or arguments bound to handlers. The only way to remove handlers from an io_service is through the io_service's destructor. Here are the relevant excerpts from the documentation:

Uninvoked handler objects that were scheduled for deferred invocation on the io_service, or any associated strand, are destroyed.

[...]

To shut down the whole program, the io_service function stop() is called to terminate any run() calls as soon as possible. The io_service destructor defined above destroys all handlers, causing all shared_ptr references to all connection objects to be destroyed.

To resolve this problem, consider decoupling the lifespan of the Boost.Asio I/O objects from TestImpl. I would personally opt to use boost::optional over boost::shared_ptr to minimize the amount of memory allocations.

TestImpl()
  : io_svc(boost::in_place()),
    update_timer(boost::in_place(boost::ref(io_svc.get()))),
    signals(boost::in_place(boost::ref(io_svc.get()), SIGINT, SIGTERM))
{
  signals->async_wait(boost::bind(&boost::asio::io_service::stop,
                                  boost::ref(io_svc))); 
};

...

void cleanup() {
  run_thread.join();
  signals      = boost::none;
  update_timer = boost::none;
  io_svc       = boost::none;
};

...

boost::optional<boost::asio::io_service> io_svc;
boost::optional<boost::asio::deadline_timer> update_timer;
boost::optional<boost::asio::signal_set> signals;
boost::thread run_thread;

Upvotes: 4

Related Questions