Destruction order vs thread safety in C++


Welcome to the next pikoTutorial !

What’s the problem?

Let’s take a look at the simple example below:

#include <iostream>
#include <future>
#include <thread>
#include <memory>
// define a class spinning up a thread
class SomeClass
{
public:
    SomeClass()
    : value_(std::make_unique<int>(12)) 
    {
        // spin up a separate thread
        future_ = std::async([this](){
            // wait for 1 second before executing any actions in the thread
            std::this_thread::sleep_for(std::chrono::seconds(1));
            std::cout << "Starting a separate thread!" << std::endl;
            std::cout << "Value: " << *value_ << std::endl;
        });
    }

    std::future<void> future_;
    std::unique_ptr<int> value_;
};

int main(int argc, char** argv)
{
    SomeClass obj;
}

Let’s quickly sum up what’s going on here and what could be the expected output of such program:

  • we have a class which holds a pointer to an integer value and which is allocated and set to 12 on the constructor’s initializer list
  • the class spins up a thread in the constructor using std::async and stores its result in a std::future, so that the std::async call does not block the code execution, but only start the thread and move forward instead
  • the created thread will always have a chance to finish because std::future calls get() in its destructor, so as soon as our class is being destroyed, the destructor of the member variable future_ also gets called which ends up in a blocking get() call which waits for the associated thread to finish
  • the thread informs us when it starts and prints the value that the member pointer points to (in this case 12)
    Nothing special here, but when we run this application we get the following output:
Starting a separate thread!
Segmentation fault (core dumped)

To understand why it happens, let’s add a custom deleter to the unique pointer, just to be able to print something at the moment when value_ gets destroyed. I’ll add also a debug print to the destructor of SomeClass:

Note: I omitted all the standard includes to save on vertical space.

// define a custom deleter
void custom_int_deleter(int* ptr)
{
    std::cout << "Destroying int pointer..." << std::endl;
    delete ptr;
}
// define a class spinning up a thread
class SomeClass
{
public:
    SomeClass()
    : value_(new int(12), &custom_int_deleter) 
    {
        // spin up a separate thread
        future_ = std::async([this](){
            // wait for 1 second before executing any actions in the thread
            std::this_thread::sleep_for(std::chrono::seconds(1));
            std::cout << "Starting a separate thread!" << std::endl;
            std::cout << "Value: " << *value_ << std::endl;
        });
    }
    ~SomeClass() { std::cout << "Destroying SomeClass..." << std::endl; }

    std::future<void> future_;
    std::unique_ptr<int, void(*)(int*)> value_;
};

int main(int argc, char** argv)
{
    SomeClass obj;
}

Note for beginners: If you don’t like function pointer syntax in the unique pointer declaration, you can replace void(*)(int*) with decltype(&custom_int_deleter). Just remember, that it’s not just a matter of personal preferences, but a matter of value which each of these ways brings. The first one abstracts you from the function name, the second one abstracts you from the function signature. The signature of the deleter is always void(T*) and that’s why I chose in the example to abstract from the deleter’s name.

Now there’s still a segmentation fault, but the output gives us more information of what’s going on:

Destroying SomeClass...
Destroying int pointer...
Starting a separate thread!
Segmentation fault (core dumped)

It looks like the pointer gets destroyed before the thread starts what means that in line 19 we’re dereferencing a deleted pointer!

The reason behind this behavior is the order of destruction of the class member variables – the same as in every other scope in C++, the member variables are destroyed in the reverse order of how they were created. By default, the construction order is just the order of the members in the class definition, so in case of our example it’s the following:

std::future<void> future_;
std::unique_ptr<int, void(*)(int*)> value_;

What means that if value_ is created after the future_, it will be destroyed before the destructor of future_ gets called, potentially allowing the associated thread to outlive the member variables which it may be using before finishing – exactly as in our example. The fix for that may be as simple as changing the order of the members in the class definition:

std::unique_ptr<int, void(*)(int*)> value_;
std::future<void> future_;

After that, the destructor of future_ will wait in a blocking way for the thread to finish before other members are destroyed:

Destroying SomeClass...
Starting a separate thread!
Value: 12
Destroying int pointer...

What about real life?

The above example was pretty easy to debug because we were explicitly using a resource after it has been explicitly deleted, but remember that in real life such kind of bugs may be much more tricky to track down because your thread’s code may look more like this:

future_ = std::async([this](){
    std::lock_guard<std::mutex> lock(mtx_);
    // lots of code and some time-consuming processing
});

After looking at this example you can say:

“Lock guard locks mutex at the beginning of the thread, so I’m fairly sure that the mutex will be valid at that point of time”.

However, in the above example it’s not the locking which is the problematic part – it’s the unlocking. See, the std::lock_guard holds a reference to the mutex which is then locked in the lock guard’s constructor and unlocked in the destructor. Are you sure that by the time the thread scope exits and lock guard attempts to unlock the mutex, the mutex will still be there?