compare_exchange C++ 函数如何确定竞争条件?
How compare_exchange C++ function determines race conditions?
我们知道,compare_exchange_weak()
returns 如果存在竞争条件,则会出现错误(假值),因此无法完全完成操作。但是 compare_exchange_weak()
究竟是如何确定竞争条件的?
如果超过一个线程尝试 read/write 值,即获取一个锁,lock cmpxchg
指令 returns 是否会出错,而正是这种方式 compare_exchange_weak
确定竞争条件?
cmpxchg
指令影响ZF
标志:如果交换成功则置位,否则清除。
让我们看一个例子:
std::atomic<int> a;
bool my_compare_exchange(int expected, int desired) {
bool succeeded = a.compare_exchange_weak(expected, desired);
return succeeded;
}
函数my_compare_exchange()
被翻译成如下汇编代码:
my_compare_exchange:
mov eax, edi
lock cmpxchg DWORD PTR a[rip], esi
sete al // <-- conditional instruction
ret
如果交换成功,寄存器 al
将使用 sete al
设置为 1
(即 ZF
由 cmpxchg
设置)。否则,它被设置为零(即 ZF
被 cmpxchg
清除)。
lock cmpxchg
是原子的;在执行期间什么也不会发生(无论如何从逻辑上讲)。与 compare_exchange_weak
的 LL/SC 实现不同,当另一个线程在同一缓存行内写入时,它不会虚假地失败,只有当比较实际失败时。 (Can CAS fail for all threads?)
compare_exchange_strong
可以实现为 lock cmpxchg
而无需循环。
您是在询问加载旧值、修改旧值然后尝试将新值 CAS 到位的用例吗? (例如,合成硬件不直接提供的原子操作,例如 , or )。
在那种情况下是的,cmpxchg
失败的原因是与另一个线程的竞争。
正如@ネロク 解释的那样,您检查 cmpxchg
的标志结果以获得 compare_exchange_strong
的布尔结果,即判断比较部分是否失败以及存储是否完成。参见 Intel's manual entry for cmpxchg
。
lock cmpxchg
也有其他用例,例如等待自旋锁通过在 lock.compare_exchange_weak(0, 1)
上自旋变得可用,反复尝试将未锁定的变量 CAS 到锁定状态。所以失败的 CAS 不是来自竞争条件,它只是来自仍然持有的锁。 (因为您要传递一个常量作为 "expected",而不是您刚刚从该位置读取的内容。)
据我所知,这通常不是一个好主意。我认为通常最好以只读方式旋转等待锁,并且只在看到可用时才尝试使用 CAS 或 XCHG 获取锁。 (使用 xchg
并测试整数结果可能比 lock cmpxchg
更便宜,可能保持缓存行锁定的周期更少。)
TL:DR:如果您要推理正确性,无锁编程需要精确的语言。
如果 EAX 在执行时与内存中的值不匹配,cmpxchg
将失败并清除 ZF(创建 ne
不等于条件)。指令本身不知道也不关心竞争,只知道它自己执行时的确切状态。
作为程序员,您在将其用作构建块以创建基于其之上的更大原子操作时必须担心竞争和重试。
我们知道,compare_exchange_weak()
returns 如果存在竞争条件,则会出现错误(假值),因此无法完全完成操作。但是 compare_exchange_weak()
究竟是如何确定竞争条件的?
如果超过一个线程尝试 read/write 值,即获取一个锁,lock cmpxchg
指令 returns 是否会出错,而正是这种方式 compare_exchange_weak
确定竞争条件?
cmpxchg
指令影响ZF
标志:如果交换成功则置位,否则清除。
让我们看一个例子:
std::atomic<int> a;
bool my_compare_exchange(int expected, int desired) {
bool succeeded = a.compare_exchange_weak(expected, desired);
return succeeded;
}
函数my_compare_exchange()
被翻译成如下汇编代码:
my_compare_exchange:
mov eax, edi
lock cmpxchg DWORD PTR a[rip], esi
sete al // <-- conditional instruction
ret
如果交换成功,寄存器 al
将使用 sete al
设置为 1
(即 ZF
由 cmpxchg
设置)。否则,它被设置为零(即 ZF
被 cmpxchg
清除)。
lock cmpxchg
是原子的;在执行期间什么也不会发生(无论如何从逻辑上讲)。与 compare_exchange_weak
的 LL/SC 实现不同,当另一个线程在同一缓存行内写入时,它不会虚假地失败,只有当比较实际失败时。 (Can CAS fail for all threads?)
compare_exchange_strong
可以实现为 lock cmpxchg
而无需循环。
您是在询问加载旧值、修改旧值然后尝试将新值 CAS 到位的用例吗? (例如,合成硬件不直接提供的原子操作,例如
在那种情况下是的,cmpxchg
失败的原因是与另一个线程的竞争。
正如@ネロク 解释的那样,您检查 cmpxchg
的标志结果以获得 compare_exchange_strong
的布尔结果,即判断比较部分是否失败以及存储是否完成。参见 Intel's manual entry for cmpxchg
。
lock cmpxchg
也有其他用例,例如等待自旋锁通过在 lock.compare_exchange_weak(0, 1)
上自旋变得可用,反复尝试将未锁定的变量 CAS 到锁定状态。所以失败的 CAS 不是来自竞争条件,它只是来自仍然持有的锁。 (因为您要传递一个常量作为 "expected",而不是您刚刚从该位置读取的内容。)
据我所知,这通常不是一个好主意。我认为通常最好以只读方式旋转等待锁,并且只在看到可用时才尝试使用 CAS 或 XCHG 获取锁。 (使用 xchg
并测试整数结果可能比 lock cmpxchg
更便宜,可能保持缓存行锁定的周期更少。
TL:DR:如果您要推理正确性,无锁编程需要精确的语言。
如果 EAX 在执行时与内存中的值不匹配,cmpxchg
将失败并清除 ZF(创建 ne
不等于条件)。指令本身不知道也不关心竞争,只知道它自己执行时的确切状态。
作为程序员,您在将其用作构建块以创建基于其之上的更大原子操作时必须担心竞争和重试。