There is a popular spin-lock mutex version which is spreaded across the Internet and which one might encounter in the Anthony Williams book(C++ Concurrency in Action). Here it is:
class SpinLock
{
std::atomic_flag locked;
public:
SpinLock() :
locked{ATOMIC_FLAG_INIT}
{
}
void lock()
{
while(locked.test_and_set(std::memory_order_acquire));
}
void unlock()
{
locked.clear(std::memory_order_release);
}
};
The thing I do not understand is why everybody uses std::memory_order_acquire
for the test_and_set
which is an RMW operation. Why is it not std::memory_acq_rel
?
Suppose we have 2 threads simultaneously trying to acquire a lock:
T1: test_and_set -> ret false
T2: test_and_set -> ret false
The situation should be possible since we have 2 acquire
operations which don't form any sync with
relationship between each other. Yes, after we have unlocked the mutex we have a release
operation which heads a subsequent release sequence
and life becomes colorful and everyone is happy. But why is it safe before the release sequence
is headed?
Since many people mention exactly that implementation I suppose it should work correctly. So what am I missing?
UPDATE 1:
I perfectly understand that the operation is atomic, that operations between lock
and unlock
can't go out of the critical section. This is not a problem. The problem is that I don't see how the code above prevents 2 mutexes coming into the critical section simultaneously. To prevent it from happening there should be happens before relationship between 2 lock
s. Could someone show me, using the C++ standard notions, that the code is perfectly safe?
UPDATE 2:
Ok, we are close to the correct answer, I believe. I've found the following in the standard:
[atomics.order] clause 11
Atomic read-modify-write operations shall always read the last value
(in the modification order) written before the write associated with
the read-modify-write operation.
And on this major note I could happily close the question but I still have my doubts. What about in the modification order
part?
Standard is pretty clear about it:
[intro.multithread] clause 8
All modifications to a particular atomic object M occur in some
particular total order, called the modification order of M . If A and
B are modifications of an atomic object M and A happens before(as defined
below) B, then A shall precede B in the modification order of M ,
which is defined below.
So according to that clause for an RMW operation to have the latest written value, the latest write operation should happen before the reading part or RMW operation. Which is not the case in the question. Right?
UPDATE 3:
I more and more think that the code for a spin lock is broken. Here is my reasoning. C++ specify 3 types of operations:
- Acquire, release, acquire-release - these are sync ops.
- Relaxed - these are no sync ops
- RMW - these are operations with "special" characteristic
Let's start with RMW and find out what so special about them. First, they are a valuable asset in forming release sequence
, second they have a special clause([atomics.order] clause 11) cited above. Nothing else special I found.
Acquire/release are sync ops and release sync with acquire
so forming a happens before
relationship. Relaxed operations are just plain atomics and don't participate in the modification order at all.
What we have in our code? We have an RMW operation which uses acquire memory semantics so whenever first unlock(release) is reached it serves 2 roles:
- It forms a
sync with
relationship with the previous release
- It participates in the
release sequence
.
But that's all true only after the first unlock
has finished.
Before that, if we have 2+ threads which are simultaneously running our lock
code then we can enter pass the lock
simultaneously since 2 acquire
operations don't form any kind of relationship. They are as unordered as relaxed operations would. Since they are unordered we can't use any special clauses about RMW operations since there is no happens before
relationship and hence no modification order for the locked
flag.
So either my logic is flawed or code is broken. Please, whoever knows the truth - comment on this.
See Question&Answers more detail:
os