理解C ++ 11中的std :: atomic :: compare_exchange_weak()

时间:2014-08-08 09:11:32

标签: c++ multithreading c++11 atomic

bool compare_exchange_weak (T& expected, T val, ..);

compare_exchange_weak()是C ++ 11中提供的比较交换原语之一。它是 ,即使对象的值等于expected,它也会返回false。这是由于某些平台上的 虚假故障 ,其中使用了一系列指令(而不是x86上的指令)来实现它。在这样的平台上,上下文切换,由另一个线程重新加载相同的地址(或高速缓存行)等可能使原语失败。它是spurious,因为它不是操作失败的对象的值(不等于expected)。相反,这是一种时间问题。

但让我感到困惑的是C ++ 11标准(ISO / IEC 14882)中的内容,

  

29.6.5   ..   虚假失败的后果是几乎所有弱的用途   比较和交换将处于循环中。

为什么它必须在 几乎所有用途 的循环中?这是否意味着我们会因为虚假失败而失败?如果是这种情况,为什么我们打扰使用compare_exchange_weak()并自己编写循环?我们可以使用compare_exchange_strong(),我认为应该摆脱我们的虚假失败。 compare_exchange_weak()的常见用例是什么?

另一个问题。在安东尼说的“C ++并发行动”一书中,

//Because compare_exchange_weak() can fail spuriously, it must typically
//be used in a loop:

bool expected=false;
extern atomic<bool> b; // set somewhere else
while(!b.compare_exchange_weak(expected,true) && !expected);

//In this case, you keep looping as long as expected is still false,
//indicating that the compare_exchange_weak() call failed spuriously.

为什么!expected处于循环状态?是否可以防止所有线程在一段时间内挨饿并且没有任何进展?

编辑:(最后一个问题)

在没有单一硬件CAS指令的平台上,弱版和强版都使用LL / SC(如ARM,PowerPC等)实现。那么以下两个循环之间有什么区别吗?为什么,如果有的话? (对我来说,他们应该有类似的表现。)

// use LL/SC (or CAS on x86) and ignore/loop on spurious failures
while (!compare_exchange_weak(..))
{ .. }

// use LL/SC (or CAS on x86) and ignore/loop on spurious failures
while (!compare_exchange_strong(..)) 
{ .. }

我提出这个最后一个问题你们都提到在循环中可能存在性能差异。它也被C ++ 11标准(ISO / IEC 14882)提及:

  

当比较和交换处于循环中时,弱版本将产生   在某些平台上表现更好。

但如上所述,循环中的两个版本应该提供相同/相似的性能。我想念的是什么?

5 个答案:

答案 0 :(得分:61)

为什么要在循环中进行交换?

通常,您希望在继续之前完成工作,因此,您将compare_exchange_weak置于循环中,以便它尝试交换直到成功(即返回true)。 / p>

请注意,compare_exchange_strong通常也用于循环中。它不会因为虚假故障而失败,但由于并发写入而失败。

为什么要使用weak代替strong

非常简单:虚假失败不会经常发生,因此不会造成很大的性能损失。相比之下,容忍这样的失败允许在某些平台上更有效地实现weak版本(与strong相比):strong必须始终检查虚假失败并掩盖它。这很贵。

因此,使用weak是因为它在某些平台上比strong快得多

您应该何时使用weak以及何时使用strong

reference个状态提示何时使用weak以及何时使用strong

  

当比较和交换处于循环中时,弱版本将产生   在某些平台上表现更好。当弱比较和交换时   需要一个循环,一个强大的不会,强大的循环   优选的。

所以答案似乎很容易记住:如果你因为虚假失败而不得不引入一个循环,那么就不要这样做;使用strong。如果您还有循环,请使用weak

为什么示例中的!expected

这取决于情况及其所需的语义,但通常不需要正确性。省略它会产生非常相似的语义。只有在另一个线程可能将值重置为false的情况下,语义可能会略有不同(但我找不到您想要的有意义的示例)。有关详细解释,请参阅Tony D.的评论。

另一个线程写true时,这只是一个快速通道:然后我们中止而不是再次尝试写true

关于你的上一个问题

  

但如上所述,循环中的两个版本应该提供相同/相似的性能。   我想念的是什么?

来自Wikipedia

  

如果没有,LL / SC的实际实现并不总是成功   对相关内存位置的并发更新。任何例外   两个操作之间的事件,例如上下文切换,另一个   加载链接,甚至(在许多平台上)另一个加载或存储   操作,将导致存储条件虚假失败。年长   如果通过广播进行任何更新,实现将失败   记忆巴士。

因此,例如,LL / SC将在上下文切换时失败。现在,强大的版本将带来它自己的小循环&#34;通过再次尝试来检测虚假故障并掩盖它。请注意,这个自己的循环也比通常的CAS循环更复杂,因为它必须区分虚假失败(并掩盖它)和由于并发访问导致的失败(导致返回值false)。弱版本没有这样的循环。

由于您在两个示例中都提供了显式循环,因此没有必要为强版本提供小循环。因此,在具有strong版本的示例中,检查失败两次;一次由compare_exchange_strong(这是更复杂,因为它必须区分虚假故障和并发访问)和一次通过你的循环。这种昂贵的检查是不必要的,这是weak在这里更快的原因。

另请注意,您的参数(LL / SC)只是一个实现此功能的可能性。有更多的平台甚至有不同的指令集。此外(更重要的是),请注意std::atomic必须支持所有可能的数据类型的所有操作,因此即使您声明了一个1000万字节的结构,也可以使用{{1}这个。即使在具有CAS的CPU上,也不能有CAS一千万字节,因此编译器将生成其他指令(可能是锁定获取,然后是非原子比较和交换,然后是锁定释放)。现在,想想在交换1000万字节时会发生多少事情。因此,虽然8字节交换可能非常罕见,但在这种情况下可能更常见。

简而言之,C ++为您提供了两种语义,一种最好的努力和#34;一个(compare_exchange)和一个&#34;我肯定会这样做,不管有多少坏事可能发生在&#34;一个(weak)。如何在各种数据类型和平台上实现这些是一个完全不同的主题。不要将您的心理模型与您特定平台上的实施联系起来;标准库旨在使用比您可能意识到的更多架构。我们可以得出的唯一一般结论是,保证成功通常比仅仅尝试并为可能的失败留出空间更困难(因此可能需要额外的工作)。

答案 1 :(得分:15)

  

为什么它必须处于几乎所有用途的循环中?

因为如果你没有循环并且它没有虚假地失败你的程序没有做任何有用的事情 - 你没有更新原子对象而你也不知道它的当前值是什么(更正:见卡梅伦的评论)。如果电话没有做任何有用的事情,那该怎么做呢?

  

这是否意味着我们会因为虚假失败而失败?

  

如果是这样的话,为什么我们打扰使用compare_exchange_weak()并自己编写循环?我们可以使用compare_exchange_strong(),我认为应该摆脱我们的虚假失败。 compare_exchange_weak()的常见用例是什么?

在某些体系结构compare_exchange_weak上效率更高,虚假故障应该相当罕见,因此可以使用弱表单和循环编写更有效的算法。

一般情况下,如果你的算法不需要循环,那么使用强版本可能会更好,因为你不必担心虚假失败。如果它甚至需要为强版本循环(许多算法确实需要循环),那么在某些平台上使用弱形式可能会更有效。

  

为什么!expected处于循环状态?

该值可能已由另一个帖子设置为true,因此您不想继续尝试设置它。

修改

  

但如上所述,循环中的两个版本应该提供相同/相似的性能。我想念的是什么?

显然,在可能发生虚假故障的平台上,compare_exchange_strong的实施必须更加复杂,以检查虚假故障并重试。

虚弱形式只会在虚假失败时返回,它不会重试。

答案 2 :(得分:13)

我在尝试通过各种在线资源(例如this onethis one),C ++ 11标准以及此处给出的答案后,自己尝试回答这个问题

合并相关问题(例如,&#34; 为什么!预期? &#34;与 &#34;合并为什么把compare_exchange_weak()放在循环中? &#34;)并给出相应的答案。


为什么compare_exchange_weak()几乎在所有用途中都必须处于循环中?

典型模式A

您需要根据原子变量中的值实现原子更新。失败表示变量未使用我们所需的值更新,我们想重试它。请注意,我们并不关心它是否由于并发写入或虚假故障而失败。但我们非常关心 是我们 进行此项更改。

expected = current.load();
do desired = function(expected);
while (!current.compare_exchange_weak(expected, desired));

一个真实的例子是几个线程同时向单个链表添加一个元素。每个线程首先加载头指针,分配一个新节点并将头部附加到这个新节点。最后,它尝试将新节点与头部交换。

另一个例子是使用std::atomic<bool>实现互斥。最多一个线程一次可以进入临界区,具体取决于首先将current设置为true并退出循环的线程。

典型模式B

这实际上是安东尼的书中提到的模式。与模式A相反,你希望原子变量更新一次,但你不关心是谁做的。只要它没有更新,你再试一次。这通常与布尔变量一起使用。例如,您需要实现状态机的触发器以继续前进。无论如何,哪个线程会触发触发器。

expected = false;
// !expected: if expected is set to true by another thread, it's done!
// Otherwise, it fails spuriously and we should try again.
while (!current.compare_exchange_weak(expected, true) && !expected);

请注意,我们通常不能使用此模式来实现互斥锁。否则,多个线程可能同时位于关键部分内。

尽管如此,在循环外使用compare_exchange_weak()应该很少见。相反,有些情况下正在使用强版本。如,

bool criticalSection_tryEnter(lock)
{
  bool flag = false;
  return lock.compare_exchange_strong(flag, true);
}

compare_exchange_weak在这里不合适,因为当它因虚假失败而返回时,可能没有人占用临界区。

饥饿线程?

值得一提的一点是,如果虚假失败继续发生会导致线程挨饿,会发生什么?理论上,当compare_exchange_XXX()作为指令序列(例如,LL / SC)实现时,它可能发生在平台上。在LL和SC之间频繁访问相同的高速缓存行将产生连续的虚假故障。一个更现实的例子是由于一个愚蠢的调度,其中所有并发线程以下列方式交错。

Time
 |  thread 1 (LL)
 |  thread 2 (LL)
 |  thread 1 (compare, SC), fails spuriously due to thread 2's LL
 |  thread 1 (LL)
 |  thread 2 (compare, SC), fails spuriously due to thread 1's LL
 |  thread 2 (LL)
 v  ..

会发生吗?

幸运的是,它不会永远发生,这要归功于C ++ 11的要求:

  

实施应该确保弱比较和交换   除非是原子,否则操作不会始终返回false   对象具有与预期不同的值或存在并发性   对原子对象的修改。

为什么我们打扰使用compare_exchange_weak()并自己编写循环?我们可以使用compare_exchange_strong()。

取决于。

案例1:当两者都需要在循环中使用时。 C ++ 11说:

  

当比较和交换处于循环中时,弱版本将产生   在某些平台上表现更好。

在x86上(至少目前。当引入更多内核时,可能它会采用类似LL / SC的类似方案来表现性能),弱版和强版基本相同,因为它们都归结为到单个指令cmpxchg。在compare_exchange_XXX()未实现原子的某些其他平台上(这里意味着没有单个硬件原语存在),循环内的弱版本可能赢得战斗,因为强大的版本将拥有处理虚假故障并相应地重试。

<强> 但是,

很少,即使在循环中,我们也可能更喜欢compare_exchange_strong()而不是compare_exchange_weak()。例如,当加载原子变量和交换计算的新值之间有很多事情要做时(参见上面的function())。如果原子变量本身不经常变化,我们不需要为每个虚假失败重复昂贵的计算。相反,我们可能希望compare_exchange_strong()&#34;吸收&#34;这样的失败,我们只会在由于真正的价值变化而失败时重复计算。

案例2:只需要在循环中使用 compare_exchange_weak() C ++ 11也说:

  

当弱比较和交换需要循环和强循环时   不会,强者更可取。

当您循环以消除弱版本的虚假故障时,通常会出现这种情况。您可以重试,直到交换成功或由于并发写入而失败。

expected = false;
// !expected: if it fails spuriously, we should try again.
while (!current.compare_exchange_weak(expected, true) && !expected);

充其量,它重新发明轮子并执行与compare_exchange_strong()相同的操作。更差? This approach fails to take full advantage of machines that provide non-spurious compare-and-exchange in hardware

最后,如果你循环寻找其他东西(例如,参见&#34;典型模式A&#34;上面),那么compare_exchange_strong()也很有可能被放入循环中,这带来了我们回到之前的案例。

答案 3 :(得分:11)

好吧,所以我需要一个执行原子左移的功能。我的处理器没有本机操作,标准库没有它的功能,所以看起来我正在编写自己的。这是:

void atomicLeftShift(std::atomic<int>* var, int shiftBy)
{
    do {
        int oldVal = std::atomic_load(var);
        int newVal = oldVal << shiftBy;
    } while(!std::compare_exchange_weak(oldVal, newVal));
}

现在,循环可能被执行多次的原因有两个。

  1. 当我做左移时,其他人改变了变量。我的计算结果不应该应用于原子变量,因为它会有效地擦掉别人的写作。
  2. 我的CPU出现故障,而弱的CAS虚假失败。
  3. 老实说,我不在乎哪一个。左移很快,我也可以再做一次,即使失败是虚假的。

    快,但是,强大的CAS需要包裹弱CAS以获得强大的额外代码。当弱CAS成功时,该代码没有太大作用......但是当它失败时,强大的CAS需要做一些侦探工作来确定它是案例1还是案例2.该侦探工作采取第二个循环的形式,有效地在我自己的循环中。两个嵌套循环。想象一下你的算法老师现在正在瞪着你。

    正如我之前提到的,我并不关心侦探工作的结果!无论哪种方式,我都要重做CAS。所以使用强大的CAS几乎没有任何收获,并且失去了一小部分但可测量的效率。

    换句话说,弱CAS用于实现原子更新操作。当您关心CAS的结果时,会使用强CAS。

答案 4 :(得分:0)

我认为以上大多数答案都将“虚假故障”视为某种问题,即性能与正确性之间的权衡。

可以看出,弱版本通常在大多数情况下速度较快,但在出现虚假故障的情况下,速度会变慢。而强版本是不可能出现虚假故障的版本,但是它几乎总是较慢。

对我来说,主要区别在于这两个版本如何处理ABA问题:

仅当没有人触摸加载和存储之间的缓存行时,弱版本才会成功,因此它将100%检测到ABA问题。

仅当比较失败时,强版本才会失败,因此,如果没有其他措施,它将无法检测到ABA问题。

因此,从理论上讲,如果在弱顺序体系结构上使用弱版本,则不需要ABA检测机制,实现会更加简单,从而提供更好的性能。

但是,在x86(强序体系结构)上,弱版本和强版本是相同的,并且都遭受ABA问题的困扰。

因此,如果编写一个完全跨平台的算法,则无论如何都需要解决ABA问题,因此使用弱版本不会带来性能优势,但是处理虚假故障会降低性能。

总而言之-出于可移植性和性能方面的考虑,强版本始终是一个更好或更平等的选择。

弱版本只有让您完全跳过ABA对策或者您的算法不关心ABA时,它才是更好的选择。