Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

std::shared_ptr Thread Safe

Is a shared pointer (std::shared_ptr) safe to use in a multi-threaded program?
I am not considering read/write accesses to the data owned by the shared pointer but rather the shared pointer itself.

I am aware that certain implementations (such as MSDN) do provide this extra guarantee; but I want to understand if this is guaranteed by the standard and as such is portable.

#include <thread>
#include <memory>
#include <iostream>

void function_to_run_thread(std::shared_ptr<int> x)
{
    std::cout << x << "\n";
}
// Shared pointer goes out of scope.
// Is its destruction here guaranteed to happen only once?
// Or is this a "Data Race" situation that is UB?

int main()
{
    std::thread   threads[2];

    {
        // A new scope
        // So that the shared_ptr in this scope has the
        // potential to go out of scope before the threads have executed.
        // So leaving the shared_ptr in the scope of the threads only.
        std::shared_ptr<int>   data = std::make_shared<int>(5);

        // Create workers.
        threads[0] = std::thread(function_to_run_thread, data);
        threads[1] = std::thread(function_to_run_thread, data);
    }
    threads[0].join();
    threads[1].join();
}

Any links to sections in the standard most welcome.

I would be happy if people have reference to the major implementations so we could consider it portable to most normal developers.

  • MSDN: Check. Thread Safe.
  • G++: ?
  • clang: ?

I would consider those the major implementations but happy to consider others.

like image 710
Martin York Avatar asked Apr 21 '26 19:04

Martin York


1 Answers

I don't have links to the standard. I did check this a long time ago, std::shared_ptr is thread-safe under certain conditions, which summarizes to: every thread should have its own copy. As documented on cppreference:

All member functions (including copy constructor and copy assignment) can be called by multiple threads on different instances of shared_ptr without additional synchronization even if these instances are copies and share ownership of the same object. If multiple threads of execution access the same shared_ptr without synchronization and any of those accesses uses a non-const member function of shared_ptr then a data race will occur.

So just like any other class in the standard, reading from the same instance from multiple threads is allowed. Writing to this instance from 1 thread is not.

int main()
{
    std::vector<std::thread>   threads;

    {
        // A new scope
        // So that the shared_ptr in this scope has the
        // potential to go out of scope before the threads have executed.
        // So leaving the shared_ptr in the scope of the threads only.
        std::shared_ptr<int>   data = std::make_shared<int>(5);

        // Perfectly legal to read access the shared_ptr
        threads.emplace_back(std::thread([&data]{ std::cout << data.get() << '\n'; }));        
        threads.emplace_back(std::thread([&data]{ std::cout << data.get() << '\n'; }));

        // This line will result in a race condition as you now have read and write on the same instance
        threads.emplace_back(std::thread([&data]{ data = std::make_shared<int>(42); }));

        for (auto &thread : threads)
           thread.join();
    }
}

Once we are dealing with multiple copies of the shared_ptr, everything is fine:

int main()
{
    std::vector<std::thread>   threads;

    {
        // A new scope
        // So that the shared_ptr in this scope has the
        // potential to go out of scope before the threads have executed.
        // So leaving the shared_ptr in the scope of the threads only.
        std::shared_ptr<int>   data = std::make_shared<int>(5);

        // Perfectly legal to read access the shared_ptr copy
        threads.emplace_back(std::thread([data]{ std::cout << data.get() << '\n'; }));        
        threads.emplace_back(std::thread([data]{ std::cout << data.get() << '\n'; }));

        // This line will no longer result in a race condition the other threads are using a copy
        threads.emplace_back(std::thread([&data]{ data = std::make_shared<int>(42); }));

        for (auto &thread : threads)
           thread.join();
    }
}

Also destruction of the shared_ptr will be fine, as every thread will call the destructor of the local shared_ptr and the last one will clean up the data. There are some atomic operations on the reference count to ensure this happens correctly.

int main()
{
    std::vector<std::thread>   threads;

    {
        // A new scope
        // So that the shared_ptr in this scope has the
        // potential to go out of scope before the threads have executed.
        // So leaving the shared_ptr in the scope of the threads only.
        std::shared_ptr<int>   data = std::make_shared<int>(5);

        // Perfectly legal to read access the shared_ptr copy
        threads.emplace_back(std::thread([data]{ std::cout << data.get() << '\n'; }));        
        threads.emplace_back(std::thread([data]{ std::cout << data.get() << '\n'; }));

        // Sleep to ensure we have some delay
        threads.emplace_back(std::thread([data]{ std::this_thread::sleep_for(std::chrono::seconds{2}); }));
    }
    for (auto &thread : threads)
       thread.join();
}

As you already indicated, the access to the data in the shared_ptr ain't protected. So similar to the first case, if you would have 1 thread reading and 1 thread writing, you still have a problem. This can be solved with atomics or mutexes or by guaranteeing read-onlyness of the objects.

like image 131
JVApen Avatar answered Apr 24 '26 22:04

JVApen



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!