We have a member method (bool try()) which is supposed to be thread-safe which decrements the variable m_count if it is greater than 0 and we try to avoid mutexes and instead use fetch_sub and atomic_thread_fence.
struct Test {
bool try() {
if (m_count<1)
return false;
int count = m_count.fetch_sub(1, std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_acquire);
return true;
}
Test():m_count(1) {}
private:
std::atomic<int> m_count;
}
We want to ensure that m_count never becomes less than 0 and that try returns true if m_count is decremented. Above two threads can decrement m_count from 1 to -1 and that is not acceptable.
There is a gap between the load if (m_count<1)
and the call to fetch_sub()
.
Say m_count == 1
and a thread executes the load and proceeds, but before it executes fetch_sub()
, a second thread executes the load and gets the same value (1
). Now both threads will execute fetch_sub()
and m_count
becomes -1
.
To eliminate this gap, you can combine the compare and the modification into a single atomic compare-and-swap (CAS) operation, like this:
bool do_try() {
bool modified=false;
int current = m_count.load(std::memory_order_relaxed);
do {
if (current == 0)
break;
assert(current > 0);
} while (!(modified = m_count.compare_exchange_weak(current, current-1, std::memory_order_relaxed)));
std::atomic_thread_fence(std::memory_order_acquire);
return modified;
}
Now m_count
cannot become -1
.
If compare_exchange()
returns false
, it updates its first argument with the latest value, so you do not have to call load()
again.