Home > Software engineering >  why do we need a reference count in this Reentrant lock example?
why do we need a reference count in this Reentrant lock example?

Time:05-31

Why do we need m_refCount in the example below? What would happen if we leaved it out and also removed the if statement and just left its body there ?

class ReentrantLock32
{
    std::atomic<std::size_t> m_atomic;
    std::int32_t m_refCount;

public:
    ReentrantLock32() : m_atomic(0), m_refCount(0) {}
    void Acquire()
    {
        std::hash<std::thread::id> hasher;
        std::size_t tid = hasher(std::this_thread::get_id());
        // if this thread doesn't already hold the lock...
        if (m_atomic.load(std::memory_order_relaxed) != tid)
        {
            // ... spin wait until we do hold it
            std::size_t unlockValue = 0;
            while (!m_atomic.compare_exchange_weak(
                unlockValue,
                tid,
                std::memory_order_relaxed, // fence below!
                std::memory_order_relaxed))
            {
                unlockValue = 0;
                PAUSE();
            }
        }
        // increment reference count so we can verify that
        // Acquire() and Release() are called in pairs
          m_refCount;
        // use an acquire fence to ensure all subsequent
        // reads by this thread will be valid
        std::atomic_thread_fence(std::memory_order_acquire);
    }
    void Release()
    {
        // use release semantics to ensure that all prior
        // writes have been fully committed before we unlock
        std::atomic_thread_fence(std::memory_order_release);
        std::hash<std::thread::id> hasher;
        std::size_t tid = hasher(std::this_thread::get_id());
        std::size_t actual = m_atomic.load(std::memory_order_relaxed);
        assert(actual == tid);
        --m_refCount;
        if (m_refCount == 0)
        {
            // release lock, which is safe because we own it
            m_atomic.store(0, std::memory_order_relaxed);
        }
    }
    bool TryAcquire()
    {
        std::hash<std::thread::id> hasher;
        std::size_t tid = hasher(std::this_thread::get_id());
        bool acquired = false;
        if (m_atomic.load(std::memory_order_relaxed) == tid)
        {
            acquired = true;
        }
        else
        {
            std::size_t unlockValue = 0;
            acquired = m_atomic.compare_exchange_strong(
                unlockValue,
                tid,
                std::memory_order_relaxed, // fence below!
                std::memory_order_relaxed);
        }
        if (acquired)
        {
              m_refCount;
            std::atomic_thread_fence(
                std::memory_order_acquire);
        }
        return acquired;
    }
};

EDIT: Example is from a book called "Game engine architecture 3rd edition" by Jason Gregory

CodePudding user response:

The count is needed to implement recursive locking. If it were not there, Release would always unlock no matter how many Acquire calls there were, that is not what you expect and want in many cases.

Consider the following common pattern:

void helper_method(){
    Acquire();
    // Work #2

    Release();
}

void method(){
    Acquire();
    // Work #1

    helper_method();

    // Work #3
    Release();
}

One has to be careful if the lock is not recursive. In that case #3 is no longer called under lock and you now have a hard to trace bug. It happens just because Release() in helper_method unlocked the lock, doing so in good faith because it locked it in the first place, not knowing it was already locked before. This is also the reason why there are std::mutex and std::recursive_mutex, locking the former twice is UB (will often deadlock in my experience).

  • Related