在cppreference atomic_compare_exchange Talk page上提出std::atomic_compare_exchange_weak
的现有实现使用非原子比较指令计算CAS的布尔结果,例如
lock
cmpxchgq %rcx, (%rsp)
cmpq %rdx, %rax
(编辑:红鲱道歉)
打破CAS循环,例如Concurrency in Action的清单7.2:
while(!head.compare_exchange_weak(new_node->next, new_node);
规范(29.6.5 [atomics.types.operations.req] / 21-22)似乎暗示比较的结果必须是原子操作的一部分:
效果:原子比较...
返回:比较结果
但实际上是否可以实现?我们应该向供应商或LWG提交错误报告吗?
答案 0 :(得分:16)
TL; DR:atomic_compare_exchange_weak在设计上是安全的,但实际的实现是错误的。
以下是Clang实际为这个小片段生成的代码:
struct node {
int data;
node* next;
};
std::atomic<node*> head;
void push(int data) {
node* new_node = new node{data};
new_node->next = head.load(std::memory_order_relaxed);
while (!head.compare_exchange_weak(new_node->next, new_node,
std::memory_order_release, std::memory_order_relaxed)) {}
}
结果:
movl %edi, %ebx
# Allocate memory
movl $16, %edi
callq _Znwm
movq %rax, %rcx
# Initialize with data and 0
movl %ebx, (%rcx)
movq $0, 8(%rcx) ; dead store, should have been optimized away
# Overwrite next with head.load
movq head(%rip), %rdx
movq %rdx, 8(%rcx)
.align 16, 0x90
.LBB0_1: # %while.cond
# =>This Inner Loop Header: Depth=1
# put value of head into comparand/result position
movq %rdx, %rax
# atomic operation here, compares second argument to %rax, stores first argument
# in second if same, and second in %rax otherwise
lock
cmpxchgq %rcx, head(%rip)
# unconditionally write old value back to next - wait, what?
movq %rax, 8(%rcx)
# check if cmpxchg modified the result position
cmpq %rdx, %rax
movq %rax, %rdx
jne .LBB0_1
比较非常安全:它只是比较寄存器。但是,整个操作并不安全。
关键点是:compare_exchange_(weak | strong)的描述说:
原子[...]如果为真,则用所需的内容替换内存点的内容,如果为false,则用预期的内存内容更新内存中的内容< / p>
或伪代码:
if (*this == expected)
*this = desired;
else
expected = *this;
请注意,expected
仅在比较为假时写入,如果比较为真,则*this
仅写入 。 C ++的抽象模型不允许在两者都被写入的情况下执行。这对于push
的正确性非常重要,因为如果发生对head
的写入,突然new_node指向其他线程可见的位置,这意味着其他线程可以开始读取{{1} (通过访问next
),如果对head->next
(别名expected
)的写入也发生了,那就是竞赛。
Clang无条件地写信给new_node->next
。在比较结果为真的情况下,这是一个发明的写作。
这是Clang中的一个错误。我不知道GCC是否会做同样的事情。
此外,该标准的措辞不是最理想的。它声称整个操作必须以原子方式进行,但这是不可能的,因为new_node->next
不是原子对象;写到那里不可能原子地发生。标准应该说的是比较和对expected
的写入以原子方式发生,但对*this
的写入则没有。但这并不是那么糟糕,因为没有人真的希望写入是原子的。
因此,应该有一个针对Clang(可能还有GCC)的错误报告,以及该标准的缺陷报告。
答案 1 :(得分:9)
我是最初发现此错误的人。在过去的几天里,我一直在向Anthony Williams发送关于此问题和供应商实施的电子邮件。我没有意识到Cubbi引发了StackOverFlow问题。不仅仅是Clang或GCC,每个供应商都被破坏了(无论如何都是重要的)。 Anthony Williams也是Just :: Thread(一个C ++ 11线程和原子库)的作者,他确认他的库正确实现(只知道正确的实现)。
Anthony已提出GCC错误报告http://gcc.gnu.org/bugzilla/show_bug.cgi?id=60272
简单示例:
#include <atomic>
struct Node { Node* next; };
void Push(std::atomic<Node*> head, Node* node)
{
node->next = head.load();
while(!head.compare_exchange_weak(node->next, node))
;
}
g ++ 4.8 [汇编程序]
mov rdx, rdi
mov rax, QWORD PTR [rdi]
mov QWORD PTR [rsi], rax
.L3:
mov rax, QWORD PTR [rsi]
lock cmpxchg QWORD PTR [rdx], rsi
mov QWORD PTR [rsi], rax !!!!!!!!!!!!!!!!!!!!!!!
jne .L3
rep; ret
clang 3.3 [汇编]
movq (%rdi), %rcx
movq %rcx, (%rsi)
.LBB0_1:
movq %rcx, %rax
lock
cmpxchgq %rsi, (%rdi)
movq %rax, (%rsi) !!!!!!!!!!!!!!!!!!!!!!!
cmpq %rcx, %rax !!!!!!!!!!!!!!!!!!!!!!!
movq %rax, %rcx
jne .LBB0_1
ret
icc 13.0.1 [assembler]
movl %edx, %ecx
movl (%rsi), %r8d
movl %r8d, %eax
lock
cmpxchg %ecx, (%rdi)
movl %eax, (%rsi) !!!!!!!!!!!!!!!!!!!!!!!
cmpl %eax, %r8d !!!!!!!!!!!!!!!!!!!!!!!
je ..B1.7
..B1.4:
movl %edx, %ecx
movl %eax, %r8d
lock
cmpxchg %ecx, (%rdi)
movl %eax, (%rsi) !!!!!!!!!!!!!!!!!!!!!!!
cmpl %eax, %r8d !!!!!!!!!!!!!!!!!!!!!!!
jne ..B1.4
..B1.7:
ret
Visual Studio 2012 [无需检查汇编程序,MS使用_InterlockedCompareExchange !!!]
inline int _Compare_exchange_seq_cst_4(volatile _Uint4_t *_Tgt, _Uint4_t *_Exp, _Uint4_t _Value)
{ /* compare and exchange values atomically with
sequentially consistent memory order */
int _Res;
_Uint4_t _Prev = _InterlockedCompareExchange((volatile long
*)_Tgt, _Value, *_Exp);
if (_Prev == *_Exp) !!!!!!!!!!!!!!!!!!!!!!!
_Res = 1;
else
{ /* copy old value */
_Res = 0;
*_Exp = _Prev;
}
return (_Res);
}
答案 2 :(得分:2)
从链接页面引用Duncan Forster:
要记住的重要一点是,CAS的硬件实现只返回1个值(旧值)而不是两个(旧的加布尔值)
所以有一条指令 - (原子)CAS - 它实际上在内存上运行,然后是另一条指令将(原子分配的)结果转换成预期的布尔值。
由于%rax
中的值是原子设置的,因此不能受到另一个线程的影响,因此这里没有竞争。
引用无论如何都是假的,因为ZF也是根据CAS结果设置的(即 返回旧值和布尔值)。不使用该标志的事实可能是错过优化,或者cmpq可能更快,但它不会影响正确性。
供参考,请考虑像这个伪代码一样分解compare_exchange_weak
:
T compare_exchange_weak_value(atomic<T> *obj, T *expected, T desired) {
// setup ...
lock cmpxchgq %rcx, (%rsp) // actual CAS
return %rax; // actual destination value
}
bool compare_exchange_weak_bool(atomic<T> *obj, T *expected, T desired) {
// CAS is atomic
T actual = compare_exchange_weak_value(obj, expected, desired);
// now we figure out if it worked
return actual == *expected;
}
你是否同意CAS是正确的原子?
如果预期的无条件商店真的是您想要询问的(而不是完全安全的比较),我同意塞巴斯蒂安认为这是一个错误。
作为参考,您可以通过强制无条件存储到本地,并使潜在可见存储再次成为条件来解决它:
struct node {
int data;
node* next;
};
std::atomic<node*> head;
void push(int data) {
node* new_node = new node{data};
node* cur_head = head.load(std::memory_order_relaxed);
do {
new_node->next = cur_head;
} while (!head.compare_exchange_weak(cur_head, new_node,
std::memory_order_release, std::memory_order_relaxed));
}
答案 3 :(得分:1)
那些人似乎不理解标准或说明。
首先,std::atomic_compare_exchange_weak
是不线程不安全的设计。这完全是胡说八道。
该设计非常清楚地定义了函数的作用以及它必须提供的保证(包括原子性和内存排序)
使用此函数的程序是否是整个线程安全的是另一回事,但函数的语义本身在原子copare交换的意义上肯定是正确的(你仍然可以编写线程) -unsafe代码使用任何可用的线程安全原语,但这是一个完全不同的故事。)
这个特殊的函数实现了线程安全的比较交换操作的“弱”版本,它与“非弱”版本的不同之处在于允许实现生成可能虚假失败的代码,如果这样可以带来性能上的好处(与x86无关)。弱并不意味着它更糟糕,它只意味着 allowable 在某些平台上更频繁地失败,如果这样可以带来整体性能优势。
当然,实现仍然需要正确。也就是说,如果比较交换失败 - 无论是通过并发还是虚假 - 它必须正确报告为失败。
其次,现有实现生成的代码与std::atomic_compare_exchange_weak
的正确性或线程安全性无关。充其量,如果生成的指令不能正常工作,这是一个实现问题,但它与语言结构无关。语言标准定义了实现必须提供的行为,它不负责正确执行它的实现。
第三,生成的代码没有问题。 x86 CMPXCHG
指令具有明确定义的操作模式。它将实际值与预期值进行比较,如果比较成功,则执行交换。您可以通过查看EAX(或x64中的RAX)或ZF
状态来了解操作是否成功。
重要的是原子比较交换是原子的,就是这种情况。无论你对结果做什么之后都不需要是原子的(在你的情况下,CMP
),因为状态不再发生变化。交换在那时成功,或者失败了。在任何一种情况下,它已经是“历史”。
std::atomic_compare_exchange_weak
具有与基础指令不同的语义,它返回bool
值。因此,您不能总是期望1:1映射到指令。编译器可能必须生成额外的指令(以及不同的指令,具体取决于您如何使用结果)来实现这些语义,但它确实对正确性没有任何影响。
唯一可以说是抱怨的事实是,它不是直接使用已经存在的ZF
状态(带有Jcc
或CMOVcc
),而是执行另一次比较。但这是一个性能问题(浪费了1个周期),而不是正确性问题。