Thread Safety of Shared Pointers' Control Block - c++

I am working on a small program that utilizes shared pointers. I have a simple class "Thing", that just is an class with an integer attribute:
class Thing{
public:
Thing(int m){x=m;}
int operator()(){
return x;
}
void set_val(int v){
x=v;
}
int x;
~Thing(){
std::cout<<"Deleted thing with value "<<x<<std::endl;
}
};
I have a simple function "fun", that takes in a shared_ptr instance, and an integer value, index, which just keeps track of which thread is outputting a given value. The function prints out the index value, passed to the function, and the reference count of the shared pointer that was passed in as an argument
std::mutex mtx1;
void fun(std::shared_ptr<Thing> t1,int index){
std::lock_guard <std::mutex> loc(mtx1);
int m=t1.use_count();
std::cout<<index<<" : "<<m<<std::endl;
}
In main,I create one instance of a shared pointer which is a wrapper around a Thing object like so:
std::shared_ptr<Thing> ptr5(nullptr);
ptr5=std::make_shared<Thing>(110);
(declared this way for exception safety).
I then create 3 threads, each of which creates a thread executing the fun() function that takes in as arguments the ptr5 shared pointer and increasing index values:
std::thread t1(fun,ptr5,1),t2(fun,ptr5,2),t3(fun,ptr5,3);
t1.join();
t2.join();
t3.join();
My thought process here is that since each of the shared pointer control block member functions was thread safe, the call to use_count() within the fun() function is not an atomic instruction and therefore requires no locking. However, both running without and without a lock_guard,this still leads to a race condition. I expect to see an output of:
1:2
2:3
3:4
since each thread spawns a new reference to the original shared pointer, the use_count() will be incremented each time by 1 reference. However, my output is still random due to some race condition.

In a multithreaded environment, use_count() is approximate. From cppreference :
In multithreaded environment, the value returned by use_count is approximate (typical implementations use a memory_order_relaxed load)
The control block for shared_ptr is otherwise thread-safe :
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.
Seeing an out-of-date use_count() is not an indication that the control-block was corrupted by a race condition.
Note that this does not extend to modifying the pointed object. It does not provide synchronization for the pointed object. Only the state of the shared_ptr and the control block are protected.

Related

Question about the thread-safety of using shared_ptr

As it's well known that shared_ptr only guarantees access to underlying control block is thread
safe and no guarantee made for accesses to owned object.
Then why there is a race condition in the code snippet below:
std::shared_ptr<int> g_s = std::make_shared<int>(1);
void f1()
{
std::shared_ptr<int>l_s1 = g_s; // read g_s
}
void f2()
{
std::shared_ptr<int> l_s2 = std::make_shared<int>(3);
std::thread th(f1);
th.detach();
g_s = l_s2; // write g_s
}
In the code snippet above, the owned object of the shared pointer named g_s are not accessed indeed.
I am really confused now. Could somebody shed some light on this matter?
std::shared_ptr<T> guarantees that access to its control block is thread-safe, but not access to the std::shared_ptr<T> instance itself, which is generally an object with two data members: the raw pointer (the one returned by get()) and the pointer to the control block.
In your code, the same std::shared_ptr<int> instance may be concurrently accessed by the two threads; f1 reads, and f2 writes.
If the two threads were accessing two different shared_ptr instances that shared ownership of the same object, there would be no data race. The two instances would have the same control block, but accesses to the control block would be appropriately synchronized by the library implementation.
If you need concurrent, race-free access to a single std::shared_ptr<T> instance from multiple threads, you can use std::atomic<std::shared_ptr<T>>. (There is also an older interface that can be used prior to C++20, which is deprecated in C++20.)

Thread safety of std::shared_ptr<std::mutex>

Consider the following class with a shared_ptr data object. The purpose is to obtain cheaply copyable objects with a data cache shared among the copies.
#include <memory> // std::shared_ptr
#include <mutex> // std::mutex, std::lock_guard
class DataClass {
std::shared_ptr<DataType> data;
std::shared_ptr<std::mutex> data_lock;
public:
DataClass() : data(std::make_shared<DataType>()), data_lock(std::make_shared<std::mutex>()) {}
DataType get_data() const {
std::lock_guard<std::mutex> lock_guard(*data_lock);
if (/* data not cached */) {
/* calculate and store data */
}
return *data;
}
};
Are calls to get_data() thread-safe? If not, how can that be achieved?
I do have code that seems to end up in a deadlock (far too large/complicated to post), which relies on the above to work fine in parallel calculations.
Are calls to get_data() thread-safe?
As long as data_lock is not modified after the initialization in the DataClass constructor, it is thread-safe. shared_ptr itself is not thread-safe beyond reference counting, meaning that if the pointer itself is modified, it must be protected with a mutex. If it remains constant (which you can ensure by marking data_lock with const) then multiple threads can read the pointer concurrently, including copying the pointer and incrementing/decrementing the reference counter.
If not, how can that be achieved?
If you do have an instance where data_lock can be modified, you must protect it with a mutex lock or otherwise guarantee that the modification cannot happen while other threads are accessing the pointer. In this particular case this would probably mean to get rid of data_lock pointer and replace it with a mutex.
You should also take care to not allow the pointed object to be accessed after data_lock is modified. Remember that dereferencing shared_ptr returns a raw reference without incrementing the reference counter. If that shared_ptr is modified, the object it previously pointed to can be destroyed and the raw references become dangling. This can happen in a subtle way, e.g. a reference to the mutex may be saved in a lock_guard, so after you modify data_lock under the lock the lock_guard destructor will attempt to unlock an already destroyed mutex.

Is std::weak_ptr<T>::lock thread-safe? [duplicate]

This question already has answers here:
About thread-safety of weak_ptr
(5 answers)
Closed 4 years ago.
Below is some sample code showing my use case. I have a PIMPL where the implementation can be shared (it's just a bunch of expensively-produced data) but the implementation can be destroyed when it's no longer needed. An instance of the class HasImpl uses a shared pointer to Impl, and the class definition includes a static weak_ptr to Impl that acts as a "pointer dispenser" to new instances of HasImpl, giving them a handle to the Impl if one exists already.
The sample has two alternatives for calling weak_ptr::lock--one that assumes that the the answer to questions 1-3 below are all "yes", and another that does not. The only reason I'd prefer that weak_ptr::lock is thread-safe is that there could be multiple threads trying to get a copy of the pointer to Impl, and if lock is thread-safe, most threads of execution won't have to pass a static variable definition (where the thread would have to check to see if it were already initialized) and won't have to compete to acquire a mutex.
/* In HasImpl.h */
class HasImpl {
public:
HasImpl();
private:
class Impl;
static std::weak_ptr<Impl> sharedImplDispenser;
std::shared_ptr<Impl> myPtrToSharedImpl;
}
/* In HasImpl.cpp */
class HasImpl::Impl {
public:
Impl(); //constructor that takes a lot of time to run
//Lots of stuff, expensively produced, accessable to HasImpl through a shared_ptr to Impl
}
/* hypothetical constructor if weak_ptr::lock is thread-safe */
HasImpl::HasImpl() : myPtrToSharedImpl{sharedImplDispenser.lock()}
{
if (!myPtrToSharedImpl) {
static std::mutex mtx;
std::lockguard<std::mutex> lck(mtx);
myPtrToSharedImpl = sharedImplDispenser.lock();
if (!myPtrToSharedImpl) {
const auto new_impl{std::make_shared<Impl()};
sharedImplDispenser = new_impl; // the only place in the program where a value is assigned to sharedImplDispenser
myPtrToSharedImpl = new_impl;
}
}
}
/* hypothetical constructor if weak_ptr::lock is not thread-safe */
HasImpl::HasImpl()
{
static std::mutex mtx;
std::lockguard<std::mutex> lck(mtx);
myPtrToSharedImpl = sharedImpl.lock();
if (!myPtrToSharedImpl) {
const auto new_impl{std::make_shared<Impl()};
sharedImplDispenser = new_impl; // the only place in the program where a value is assigned to sharedImplDispenser
myPtrToSharedImpl = new_impl;
}
}
Assuming that std::weak_ptr is not empty and was assigned a pointer sometime in the distant past, will the control block be ok if one thread calls weak_ptr::lock while another thread may be calling weak_ptr::lock?
Is calling weak_ptr::lock while another thread may be assigning a ptr to an empty weak_ptr safe enough? That is, will the value either return nullptr or the new pointer? I don't care if the nullptr is spurious (that is, that assignment has occurred but the other threads don't know about it yet). I just don't want to corrupt the control block or obtain an invalid pointer value from the call.
Is calling weak_ptr::lock while the last shared_ptr to the object is being destroyed thread safe?
If there are problems with 1 through 3, will std::atomic<std::weak_ptr<T>> in C++20 fix the issue?
The standard explicitly says that weak_ptr::lock is "executed atomically". So that answers 1 and 3.
For #2, if you're asking about assigning to the same weak_ptr, then it's a data race. Operations that change a shared state's use_count don't provoke a data race, but copying or manipulating the weak_ptr itself is doing more than just poking at use_count.
But if you're talking about locking one weak_ptr while nulling out a different weak_ptr that are both talking to the same shared state, that's fine. The two only interact through the shared state's count, which is stated to be fine.
And yes, atomic<weak_ptr<T>> would allow you to manipulate the same object from multiple threads.

Is Poco RefCountedObject thread safe?

Poco RefCountedObject offer 2 interfaces:
inline void RefCountedObject::duplicate() const
{
++_counter;
}
inline void RefCountedObject::release() const throw()
{
try
{
if (--_counter == 0) delete this;
}
catch (...)
{
poco_unexpected();
}
}
With:
class Foundation_API RefCountedObject
/// A base class for objects that employ
/// reference counting based garbage collection.
///
/// Reference-counted objects inhibit construction
/// by copying and assignment.
{
public:
RefCountedObject();
/// Creates the RefCountedObject.
/// The initial reference count is one.
void duplicate() const;
/// Increments the object's reference count.
void release() const throw();
/// Decrements the object's reference count
/// and deletes the object if the count
/// reaches zero.
int referenceCount() const;
/// Returns the reference count.
protected:
virtual ~RefCountedObject();
/// Destroys the RefCountedObject.
private:
RefCountedObject(const RefCountedObject&);
RefCountedObject& operator = (const RefCountedObject&);
mutable AtomicCounter _counter;
};
Note that:
mutable AtomicCounter _counter;
my question is if I use a RefCountedObject in multithreading is it safe?
In my opinion it's not because only --_counter is atomic but if(--_count) is not atomic and could lead to having a reference to a deleted object.
For example let say I have 2 threads A and B one executing duplicate and the other release, the execution sequence is as follow:
B start executing release and reach --_counter
A start executing duplicate and reach ++_counter
At this point _counter=1
B execute --_counter and return the result to be evaluated by the if (which is 0 && counter is 0 now)
B is preempted and stop before the branching statement (the if)
A execute _counter++ and return a reference to the object
B continue and evaluate the branching statement with value 0 and delete the object
We end up with A having a reference to a deleted object.
even if the mutable keyword force the compiler to not optimize the _counter it won't help in multithreading
Am I missing something?
Strictly from looking at duplicate and release in isolation, the above is correct.
However, if you have two threads, both having a pointer to the same RefCountedObject, you either should have separate AutoPtr instances in each thread (which implies the _counter is > 1 to begin with), or, if you share the same AutoPtr, it must be protected by a mutex, if at least one thread can change the AutoPtr (which would result in release() being called). Needless to say sharing an mutable AutoPtr between multiple threads is a recipe for disaster.
If you manage RefCountedObject by manually calling duplicate() and release() (which I would not recommend), you should follow POCO's reference counting rules and be very careful.
So, for well-behaved code, RefCountedObject together with AutoPtr is safe for multithreaded use.
It is safe because the counter is based on std::atomic AND std::atomic is using by default std::memory_order_seq_cst for its operation.
If std::atomic was using std::memory_order_relaxed, then the implementation would be buggy/unsafe.
The downside, is that std::memory_order_seq_cst is "slow". There are faster implementations than Poco's, but less simple, by using some different code and different memory ordering commands (https://en.cppreference.com/w/cpp/atomic/memory_order)

Locking shared resources in constructor and destructor

I believe I've got a good handle on at least the basics of multi-threading in C++, but I've never been able to get a clear answer on locking a mutex around shared resources in the constructor or the destructor. I was under the impression that you should lock in both places, but recently coworkers have disagreed. Pretend the following class is accessed by multiple threads:
class TestClass
{
public:
TestClass(const float input) :
mMutex(),
mValueOne(1),
mValueTwo("Text")
{
//**Does the mutex need to be locked here?
mValueTwo.Set(input);
mValueOne = mValueTwo.Get();
}
~TestClass()
{
//Lock Here?
}
int GetValueOne() const
{
Lock(mMutex);
return mValueOne;
}
void SetValueOne(const int value)
{
Lock(mMutex);
mValueOne = value;
}
CustomType GetValueTwo() const
{
Lock(mMutex);
return mValueOne;
}
void SetValueTwo(const CustomType type)
{
Lock(mMutex);
mValueTwo = type;
}
private:
Mutex mMutex;
int mValueOne;
CustomType mValueTwo;
};
Of course everything should be safe through the initialization list, but what about the statements inside the constructor? In the destructor would it be beneficial to do a non-scoped lock, and never unlock (essentially just call pthread_mutex_destroy)?
Multiple threads cannot construct the same object, nor should any thread be allowed to use the object before it's fully constructed. So, in sane code, construction without locking is safe.
Destruction is a slightly harder case. But again, proper lifetime management of your object can ensure that an object is never destroyed when there's a chance that some thread(s) might still use it.
A shared pointer can help in achieving this eg. :
construct the object in a certain thread
pass shared pointers to every thread that needs access to the object (including the thread that constructed it if needed)
the object will be destroyed when all threads have released the shared pointer
But obviously, other valid approaches exist. The key is to keep proper boundaries between the three main stages of an object's lifetime : construction, usage and destruction. Never allow an overlap between any of these stages.
They don't have to be locked in the constructor, as the only way anyone external can get access to that data at that point is if you pass them around from the constructor itself (or do some undefined behaviour, like calling a virtual method).
[Edit: Removed part about destructor, since as a comment rightfully asserts, you have bigger issues if you're trying to access resources from an object which might be dead]