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.
I would consider those the major implementations but happy to consider others.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With