Reputation: 275220
So I have a simple cow_ptr
. It looks something like this:
template<class T, class Base=std::shared_ptr<T const>>
struct cow_ptr:private Base{
using Base::operator*;
using Base::operator->;
using Base::operator bool;
// etc
cow_ptr(std::shared_ptr<T> ptr):Base(ptr){}
// defaulted special member functions
template<class F>
decltype(auto) write(F&& f){
if (!unique()) self_clone();
Assert(unique());
return std::forward<F>(f)(const_cast<T&>(**this));
}
private:
void self_clone(){
if (!*this) return;
*this = std::make_shared<T>(**this);
Assert(unique());
}
};
this guarantees that it holds a non-const T
and ensures it is unique
when it .write([&](T&){})
s to it.
The c++17 deprecation of .unique()
seems to indicate this design is flawed.
I am guessing that if we start with a cow_ptr<int> ptr
with 1
in thread A, pass it to thread B, make it unique, modify it to 2
, pass ptr
it back and read it in thread A
we have generated a race condition.
How do I fix this? Can I simply add a memory barrier in write
? Which one? Or is the problem more fundamental?
Are symptoms less likely on x86 due to the x86 memory consistency going above and beyond what C++ demands?
Upvotes: 18
Views: 1186
Reputation: 249
unique() was deprecated, because it is not thread safe: p0521r0
I think the same is true for std::shared_ptr::use_count(), but it was not deprecated. See comment on cppreference.com
In the end std::shared_ptr is not 100% thread-safe. You should use lock guards, if you access, copy or reset/destroy a single shared_ptr instance from multiple threads. So, I agree with your guess. Thread safety of shared_ptr:
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 instance of shared_ptr without synchronization and any of those accesses uses a non-const member function of shared_ptr then a data race will occur; the shared_ptr overloads of atomic functions can be used to prevent the data race.
Regarding your suggested fix:
I do not think, that introducing locks or memory barriers (you mean atomic variables?) in your existing class will change anything regarding thread-safety. Your class inherits shared_ptr which is not thread safe in all use-cases. This has nothing to do with your unique() call.
To be clear:
Calls from multiple threads on local copies of shared_ptr, which point to the same resource are thread safe.
Calls from multiple threads on a single shared_ptr instance are not thread safe.
Upvotes: 1
Reputation: 47619
I think the idea to deprecate this is that it can't be used incorrectly like if you had code like this:
if(sp.unique()) {
// some deinitialisation
} else {
// somebody else will deinitialise.
}
It is possible that it'll fail to deinitialise if it happens to run 2 times simultaneously.
In your particular case I see no problem, because
I don't think there's any other issue with orders to access to memory since counters in shared_ptr
are atomic.
I'd probably just switch to use_count == 1
may be with appropriate comment
Upvotes: 0