使用GCC同步内置函数的CAS原子交换

6

比较并交换函数能否用于原子交换变量? 我正在使用通过gcc在x86_64 RedHat Linux上的C/C++,具体来说是__sync内置函数。 例如:

   int x = 0, y = 1; 
   y = __sync_val_compare_and_swap(&x, x, y);

我认为这归结于x是否可以在&x和x之间变化;例如,如果&x构成一个操作,那么x可能会在参数中在&x和x之间变化。我想假设上面隐含的比较总是正确的;我的问题是我能不能这样做。显然,有CAS的bool版本,但是这样我就无法将旧的x写入y。
一个更有用的例子可能是向链表头插入或删除元素(gcc声称支持指针类型,因此假设elem和head是指针)。
   elem->next = __sync_val_compare_and_swap(&head, head, elem); //always inserts?
   elem = __sync_val_compare_and_swap(&head, head, elem->next); //always removes?

Reference: http://gcc.gnu.org/onlinedocs/gcc/Atomic-Builtins.html

3个回答

3
由于与另一个同时更改目标值的线程产生竞争,该操作可能实际上不会将新值存储到目标中。CAS原语不能保证写入发生 - 只有在值已经符合预期时才会发生写入。如果值不符合预期,则原语无法确定正确的行为,因此在这种情况下什么也不会发生 - 您需要通过检查返回值来修复问题以查看操作是否成功。
因此,您的示例:
elem->next = __sync_val_compare_and_swap(&head, head, elem); //always inserts?

这段代码不一定会插入新元素。如果另一个线程在同一时刻插入了一个元素,就会出现竞争条件,可能会导致该线程调用__sync_val_compare_and_swap()时无法更新head(但是如果正确处理,这个线程和其他线程的元素都还没有丢失)。

但是,这行代码还有另一个问题——即使head被更新了,在短暂的时间内,head指向插入的元素,但该元素的next指针尚未更新为指向列表的前一个头部。如果另一个线程在那个时候插入并尝试遍历列表,就会发生错误。

为了正确地更新列表,请将该行代码更改为以下内容:

whatever_t* prev_head = NULL;
do {
    elem->next = head;  // set up `elem->head` so the list will still be linked 
                        // correctly the instant the element is inserted
    prev_head = __sync_val_compare_and_swap(&head, elem->next, elem);
} while (prev_head != elem->next);

或者使用bool变量,我认为这更加方便:

do {
    elem->next = head;  // set up `elem->head` so the list will still be linked 
                        // correctly the instant the element is inserted
} while (!__sync_bool_compare_and_swap(&head, elem->next, elem));

这段代码有点丑陋,希望我理解得没错(线程安全代码的细节很容易出错)。它应该被包装在一个 insert_element() 函数中(或者更好的是,使用适当的库)。

解决 ABA 问题:

我认为 ABA 问题与这个“将元素添加到列表头部”的代码无关。假设一个线程想要将对象 X 添加到列表中,并且在执行 elem->next = head 时,head 的值为 A1

然后,在执行 __sync_val_compare_and_swap() 之前,另一组线程会进行以下操作:

  • 从列表中删除 A1,使 head 指向 B
  • 对对象 A1 进行任何操作并释放它
  • 分配另一个对象 A2,恰好位于与 A1 相同的地址上
  • A2 添加到列表中,使 head 现在指向 A2

由于 A1A2 具有相同的标识符/地址,这是 ABA 问题的一个实例。

然而,在这种情况下,它并不重要,因为添加对象 X 的线程并不关心 head 是否指向与开始时不同的对象 - 它所关心的是当 X 被排队时:

  • 列表是一致的,
  • 列表中没有丢失任何对象,和
  • 除了 X(由此线程)之外,没有添加其他对象到列表中

@ddoman:你能详细说明一下你如何修复它吗?或者你会只使用维基百科关于ABA问题提出的方法之一吗? - Aktau
我不确定ABA问题是否是这里的问题 - 我已经更新了答案以说明原因。如果我错了,我希望能得到详细信息。 - Michael Burr

0

不行。在x86上,CAS指令从寄存器中取出一个值,并将其与内存中的一个值进行比较/写入。

为了原子地交换两个变量,它必须使用两个内存操作数。

至于x是否可以在&xx之间更改?是的,当然可以。

即使没有&,它也可能会改变。

即使在像Foo(x, x)这样的函数中,您也可以获得两个不同的x值,因为为了调用函数,编译器必须:

  • 根据调用约定将x的值存储在第一个参数的位置
  • 根据调用约定将x的值存储在第二个参数的位置

在这两个操作之间,另一个线程很容易修改x的值。


0

看起来你正在寻找交换原语,而不是比较交换原语。这将无条件地原子地交换持有寄存器和目标内存位置。

然而,你仍然面临着对 y 赋值时的竞争条件问题。有时候 y 是一个本地变量,在这种情况下它是安全的,但如果 xy 都是共享的,那么你就会遇到一个严重的问题,需要使用锁来解决。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接