std :: atomic_compare_exchange_weak线程不安全吗?

时间:2014-02-19 11:37:31

标签: c++ c++11 atomic

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提交错误报告吗?

4 个答案:

答案 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状态(带有JccCMOVcc),而是执行另一次比较。但这是一个性能问题(浪费了1个周期),而不是正确性问题。