对于锁(自旋锁、互斥锁),我们通常需要在加锁时添加acquire fence以保证锁的功能。
但这对于一次性自旋锁来说是必要的吗?例如:
int val = 0;
int lock = 0;
void thread0(void)
{
int tmp = 0;
if (atomic_compare_exchange_strong_explicit(&lock, &tmp, 1, memory_order_relaxed, memory_order_relaxed)) { // do we need memory_order_acquire here ?
assert(!val); // will it always success?
val = 1;
}
}
// same as thread0
void thread1(void)
{
int tmp = 0;
if (atomic_compare_exchange_strong_explicit(&lock, &tmp, 1, memory_order_relaxed, memory_order_relaxed)) {
assert(!val);
val = 1;
}
}
更具体地说,以下代码在armv7-a架构上是否正确(与上面提到的C代码可能存在一些差异):
val:
.long 0
lock:
.long 0
core0:
mov r0, #val
mov r1, #lock
mov r4, #1
2:
ldrex r2, [r1]
cmp r2, #0
beq 1f
bx lr // ret
1:
strex r3, r4, [r1]
cmp r3, #0
bne 2b
// without acquire fence
ldr r5, [r0] // is r5 != 0 allowed?
core1:
mov r0, #val
mov r1, #lock
mov r4, #1
2:
ldrex r2, [r1]
cmp r2, #0
beq 1f
bx lr // ret
1:
strex r3, r4, [r1]
cmp r3, #0
bne 2b
dmb ish // acquire fence
str r4, [r0] // store 1
断言总是成功,因为只有一个线程或另一个线程运行
if
主体,并且它在同一线程中的 val=1
之前排序。
原子 RMW 决定哪个线程赢得竞争,但它不需要与任何先前的编写器同步以防止其“关键部分”重叠。 (https://preshing.com/20120913/acquire-and-release-semantics/)
你没有关键部分。
val
的加载可能发生在 CAS 之前,但这仍然没问题,因为那时您仍然会加载初始值。 并且没有 release
存储,因此其他线程不会知道您的 val
更新已完成。
我不会将其称为一次性自旋锁,这可能会产生误导性的影响,例如您将与
lock
变量同步。 (顺便说一句,lock
需要下注atomic_int
又名_Atomic int
不简单int
。)