Search code examples
c++c++11smart-pointers

Race condition example about shared_ptr


Why there is no race condition in the code snippet below,

#include<memory>
#include<thread>

std::shared_ptr<int> g_s = std::make_shared<int>(1);

void f1(std::shared_ptr<int> sp)
{
    std::shared_ptr<int>l_s1 = sp; // read g_s
}

void f2()
{
    std::shared_ptr<int> l_s2 = std::make_shared<int>(3);
    std::thread th(f1, g_s);
    th.detach();
    g_s = l_s2; // write g_s
}

int main()
{
    std::thread(f2).join();
}

whereas there is a race condition in the code snippet below?

#include<memory>
#include<thread>

std::shared_ptr<int> g_s = std::make_shared<int>(1);

void f1(std::shared_ptr<int>& sp)
{
    std::shared_ptr<int>l_s1 = sp; // read g_s
}

void f2()
{
    std::shared_ptr<int> l_s2 = std::make_shared<int>(3);
    std::thread th(f1, std::ref(g_s));
    th.detach();
    g_s = l_s2; // write g_s
}

int main()
{
    std::thread(f2).join();
}

My current thought about this question is seen at the first answer. But I am not so sure yet. Could somebody please shed some light this matter?


Solution

  • std::shared_ptr itself is not thread-safe, meaning that Same object (instance) of std::shared_ptr<> can't be modified same time from two threads.

    It is not thread safe because there is no mutex inside implementation of shared_ptr<>, for the sake of speed.

    Your first code snippet passes around only copies of shared_ptr<> instance, never references, hence two threads modify different copies of them. Which is thread safe.

    Second code snippet passes reference to second thread, hence two threads modify same instance of shared_ptr<> which is not thread safe.

    Why modifying a copy of shared pointer is thread safe? We need to look into how it is implemented.

    Shared pointer consists of object's pointer itself and the pointer which points to the structure that conatins two counters, one counts number of references of shared pointer, second counts number of weak pointer references. Counters are usually allocated on Heap, so that pointer to same heaped counters can be shared between several copies of shared pointer.

    When copy of shared pointer is made, heap pointer to its counters is copied to other copy of shared pointer, and shared pointer counter is incremented Atomically.

    When copy of shared pointer is destroyed, its counter is decremented Atomically. And if it was very last copy then counters are deallocated from heap.

    Why counter should be incremented/decremented Atomically? Because same time other copy of shared pointer in other thread might be constructed or destroyed, hence same time other thread will increment/decrement counter.

    If counter is incremented/decremented on different threads same time, then it should happen Atomically, so that value is read-modified-stored as one single operation. This is needed to avoid race conditions within counter itself.

    Atomic operations are usually done with the help of std::atomic.