Some time ago, I had an object whose ownership I wanted to share. Here is a very contrived example:
class Foo
{
public:
void SetInfo(const std::string& value) { info = value; }
std::string GetInfo() { return info; }
private:
std::string info;
};
class Bar
{
void SetFoo(std::shared_ptr<Foo> aFoo) { foo = aFoo; }
void doUpdate()
{
auto myFoo = foo;
if (myFoo)
{
doScreenUpdate(*myFoo);
}
}
void doScreenUpdate(const Foo& aFoo)
{
std::cout << aFoo.GetInfo() << "\n";
}
private:
std::shared_ptr<Foo> foo;
};Two threads use Bar: one only reads values and occasionally updates the screen, the other updates values as they come in. I had learned from Herb Sutter’s talk ‘Writing Good C++14 By Default’ at CppCon 2015 (from 0:58:00 onwards) that you should not use a member shared_ptr directly, because it could be invalidated while you are using it. So, like a good programmer, I copy it into myFoo before using it. Assuming SetFoo() is called before doUpdate(), for example before any thread is started, there is no problem.
However, let’s look at what happens if SetFoo() is called while doUpdate() is in progress. If SetFoo() sets foo to a new value before auto myFoo = foo;, then myFoo will be valid and doScreenUpdate() will simply show the new value; otherwise it will show the old value, right? And what if we call SetFoo(nullptr);? If that happens before foo is assigned to myFoo, nothing will happen; otherwise it will show the old value. Or so I thought…
The actual answer: no, it is undefined behaviour in all cases.
The assumption Herb made in his talk is that the modifier of foo is a recursive call on the same thread. In this case, however, the modifier runs on another thread, and things get more complicated.
After assigning foo, myFoo contains a copy of the old value of foo, a copy of the new value, or a mix of the two. The first time I saw this behaviour, I thought something must be terribly wrong with my program, and it turns out there was.
The problem here is that access to a shared_ptr<> is not atomic.
Implementations of shared_ptr can vary, but in VS2017 its size is 8 bytes in x86 mode and 16 bytes in x64 mode. It basically comes down to two pointers: one to the actual object and one to the internal shared state. This means that if you copy the shared_ptr while it is being assigned a new value at the same time, you may end up with the object pointer of one and the shared state of the other. Needless to say, this is very bad 🙂
I could go into possible solutions for this problem, but the actual lesson to be learned here is: do not access the same shared_ptr from multiple threads. I got this confirmed by STL:
std::shared_ptr<>protects its refcount, so simultaneous reads/copies of the same shared_ptr, or reads/writes of different shared_ptrs pointing to the same object, are OK.
So the refcount is protected and you can safely copy a shared_ptr across threads, but you cannot write to the same shared_ptr object from multiple threads. Again, STL:
The usual thread safety guarantees apply. Multiple threads simultaneously reading/writing a single object = bad.
If you really have to, make sure to use a synchronization primitive such as a mutex to make the access exclusive. As a final note: std::atomic will unfortunately not help us here, as it only operates on trivially copyable types (bool, int, double, etc.).
In C++17 we were expected to get std::atomic_shared_ptr<>, which would allow this kind of usage. (Update: we didn’t get std::atomic_shared_ptr<> in C++17, but it is proposed for C++20.) The only current implementation that I know of is a lock-free one in the just::thread Pro library by Anthony Williams: https://www.stdthread.co.uk.
Other related resources:
Leave a Reply