Search code examples
c++multithreadingshared-ptr

Is shared_ptr::unique() indicative that only one thread owns it?


I have a worker thread giving my rendering thread a std::shared_ptr<Bitmap> as part of how I download texture data from the GPU. Both threads depend on std::shared_ptr<...>::unique() to determine if the other thread is done with the Bitmap. No other copies of this shared pointer are in play.

Here's a trimmed down chart of the behavior:

          worker thread             |            rendering thread
----------------------------------- + -------------------------------------------------
std::shared_ptr<Bitmap> bitmap;     |  std::queue<std::shared_ptr<Bitmap>> copy;
{                                   |  {
    std::scoped_lock lock(mutex);   |     std::scoped_lock lock(mutex);
    queue.push_back(bitmap);        |     std::swap(queue, copy);
}                                   |  }
...                                 |  for(auto it = copy.begin(); it != copy.end(); ++it) 
                                    |     if (it->unique() == false)
if (bitmap.unique())                |         fillBitmap(*it); // writing to the bitmap
    bitmap->saveToDisk(); // reading       

Is it safe to use unique() or use_count() == 1 to accomplish this?

edit:

Ah... since unique() is unsafe in this case, I think instead I'm going to try something like std::shared_ptr<std::pair<Bitmap, std::atomic<bool>>> bitmap; instead, flipping the atomic when it's filled.


Solution

  • No, it is not safe. A call to unique (or to use_count) does not imply any synchronization.

    If e.g. the worker thread observes bitmap.unique() as true, this does not imply any memory ordering on the accesses made by fillBitmap(*it); and those made by bitmap->saveToDisk();. The function call may be implemented as a relaxed load, which doesn't have the required acquire-release effect.

    Without any memory ordering, you will likely have a data race and undefined behavior as consequence.

    This is why std::shared_ptr::unique was deprecated with C++17 and removed with C++20, since it is so easy to misuse in this way. Just using use_count() == 1 doesn't work either. It has the exact same issues. I guess it wasn't removed as well, because there may be valid uses cases even in multi-threaded environments for use_count if you only need it to get a fuzzy idea of the number of current users and because there should still be a way of implementing unique for a single-threaded context (where it doesn't have any problems).

    According to the proposal to remove std::shared_ptr::unique, many implementations do in fact use a relaxed load, implying that problems may actually happen on real implementations, see https://open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0521r0.html.


    If you are on an architecture like x86 you may be saved by the fact that on the hardware level a relaxed load is automatically an acquire load.

    However, the C++ memory model does not make any ordering guarantees here and I think (might be wrong here) for example ARM is an example for an architecture where normal (relaxed) loads don't automatically have acquire/release semantics.

    And in any case, because there are no memory ordering guarantees, I think the compiler could still perform transformations that will cause issues in any case. For example, because unqiue doesn't imply any synchronization, the compiler could load data used in bitmap->saveToDisk(); before actually loading the reference counter. The load may be unnecessary if the condition on the reference counter turns out to not be fulfilled, but I don't see anything preventing the compiler from adding a superfluous load.

    Inbetween these loads some stores from fillBitmap(*it); and the destruction of the rendering threads std::shared_ptr could happen, causing the worker thread to use data from before the stores in fillBitmap(*it);.