Search code examples
c++multithreadingthread-safetystdatomic

how to decrement mutithreaded using fetch_sub and atomic_thread_fence?


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.


Solution

  • 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.