myoldgrandpa
myoldgrandpa

Reputation: 1019

How to implement simple custom lock using acquire-release memory order model?

I'm trying to understand acquire-release memory order through implementing my custom lock.

#include <atomic>
#include <vector>
#include <thread>
#include <iostream>

class my_lock {
    static std::atomic<bool> flag;
public:
    void lock() {
        bool expected = false;
        while (!flag.compare_exchange_strong(expected, true, std::memory_order_acq_rel))
            expected = false;
    }

    void unlock() {
        flag.store(false, std::memory_order_release);
    }
};

std::atomic<bool> my_lock::flag(false);

static int num0 = 0;
static int num1 = 0;

my_lock lk;

void increase() {
    for(int i = 0; i < 100000; ++i) {
        lk.lock();

        num0++;
        num1++;
        lk.unlock();
    }
}

void read() {
    for(int i = 0; i < 100000; ++i) {
        lk.lock();
        if(num0 > num1) {
            std::cout << "num0:" << num0 << " > " << "num1:" << num1 << std::endl;
        }
        lk.unlock();
    }
}

int main() {

    std::thread t1(increase);
    std::thread t2(read);
    t1.join();
    t2.join();
    std::cout << "finished! num0:" << num0 << ", num1:"<< num1 << std::endl;

}

Question 1: Am I correct to use acquire-release memory order ?

Below are paragraph from C++ Concurrency In Action

Despite the potentially non-intuitive outcomes, anyone who’s used locks has had to deal with the same ordering issues: locking a mutex is an acquire operation, and unlocking the mutex is a release operation. With mutexes, you learn that you must ensure that the same mutex is locked when you read a value as was locked when you wrote it, and the same applies here; your acquire and release operations have to be on the same variable to ensure an ordering. If data is protected with a mutex, the exclusive nature of the lock means that the result is indistinguishable from what it would have been had the lock and unlock been sequentially consistent operations. Similarly, if you use acquire and release orderings on atomic variables to build a simple lock, then from the point of view of code that uses the lock, the behavior will appear sequentially consistent, even though the internal operations are not.

This paragraph says that "the result is indistinguishable from ..... sequentially consistent operation".

Question 2: Why the result is indistinguishable? If we use lock, the result should be distinguishable from my understanding.

Edit :

I add one more question. Below is std::atomic_flag example in C++ concurrency in action.

class spinlock_mutex
{
    std::atomic_flag flag;
public:
    spinlock_mutex():
        flag(ATOMIC_FLAG_INIT)
    {}
    void lock()
    {
        while(flag.test_and_set(std::memory_order_acquire));
    }
    void unlock()
    {
        flag.clear(std::memory_order_release);
    }
}

Question 3: Why does this code doesn't use std::memory_order_acq_rel? flag.test_and_set is RWM operation, so I think it should be used with std::memory_order_acq_rel like my first example.

Upvotes: 0

Views: 556

Answers (1)

pveentjer
pveentjer

Reputation: 11392

The acquire/release is strong enough to confine the instructions within the critical sections. And it provides sufficient synchronization such that a happens before relation is established between a release of a lock and subsequent acquire of the same lock.

The lock will give you some sequential order on the lock acquire/release operations; just as with sequential consistency.

Why are you using compare_exchange_strong? You are already in a loop. You can use compare_exchange_weak.

Upvotes: 0

Related Questions