Home > OS >  Race condition example about shared_ptr
Race condition example about shared_ptr

Time:05-21

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?

CodePudding user response:

Firstly, std::shared_ptr guarantees access to underlying control block is thread safe.

For the latter code snippet, both the raw pointer and the pointer to the control block may be concurrently read from and written to by different threads, which would constitute a race.

For the former one, since void f1(std::shared_ptr<int> sp) passes the parameter(i.e std::shared_ptr<T> sp) by value, reading both the raw pointer and the pointer to the control block of sp is independent from assigning the said pointers to g_s. So there is no data race condition.

CodePudding user response:

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 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.

CodePudding user response:

To help the readers who want to better understand about the data race about shared_ptr.

This code snippet does not have any race condition:

#include<memory>
#include<thread>

std::shared_ptr<int> g_s = std::make_shared<int>(1);
std::weak_ptr<int> w_p{g_s};

void f1(std::weak_ptr<int>& wp)
{
    std::shared_ptr<int>l_s1 = wp.lock(); // read g_s
}

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

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

This code snippet below does not have any race condition, either:

#include<memory>
#include<thread>

std::shared_ptr<int> g_s = std::make_shared<int>(1);
std::weak_ptr<int> w_p{g_s};

void f1(std::weak_ptr<int> wp)
{
    std::shared_ptr<int>l_s1 = wp.lock(); // read g_s
}

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

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

Since the std::weak_ptr::lock() creates a new std::shared_ptr that shares ownership of the managed object. If there is no managed object, i.e. *this is empty, then the returned shared_ptr also is empty., wp.Lock() returns a temporary shared_ptr, which is different from the one named g_s.

  • Related