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.
CodePudding user response:
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);
.