I know that the typical recommended implementation of std::shared_ptr implies that the counter is incremented with the relaxed mode while the decrement operation needs the release mode (and, if it happens that it is the last reference, the acquire fence is followed by the deletion of a control block).
Consider the following code:
#include <atomic>
#include <thread>
#include <cassert>
#include <memory>
std::atomic_bool ab{false};
int main()
{
auto iPtr = std::make_shared<int>(5);
std::thread thr{[iPtr] // here is sth like counter.fetch_add(1, std::memory_order::relaxed)
{
while (!ab.load(std::memory_order::relaxed));
assert(*iPtr == 6); // B
// (destructor) here in sth like counter.fetch_sub(1, std::memory_order_release) followed by std::atomic_thread_fence(std::memory_order_acquire)
}};
*iPtr = 6; // A
iPtr.reset(); // here is sth like counter.fetch_sub(1, std::memory_order_release)
ab.store(true, std::memory_order::relaxed);
thr.join();
}
Could the assert actually fail?
My assumption would be yes. Although we perform a release operation in iPtr.reset();
, we do not have a corresponding acquire operation before the assert which would “update” the memory, so we can’t say that A synchronizes-with and, therefore, happens-before B.
Am I right in my understanding?
Note: I set the relaxed mode for ab
on purpose, so that there woudn’t be unexpected synchronized-with relationship formed by it. Therefore, the only synchronizations that really take place are those induced by the shared_ptr’s counter