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

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

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

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

29.6.5 ..虚假失败的后果是几乎所有弱比较交换的使用都将在一个循环中。

为什么在几乎所有用途中都必须处于循环状态? 这是否意味着当虚假故障导致失败时我们会循环? 如果是这样的话,为什么我们打扰使用compare_exchange_weak()并自己写循环呢? 我们可以使用compare_exchange_strong() ,我认为应该为我们摆脱虚假的失败。 compare_exchange_weak()的常见用例是什么?

另一个问题有关。 Anthony在他的书“C ++ Concurrency In Action”中说:

 //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. 

为什么在循环条件下呢? 是否有阻止所有线程在一段时间内挨饿而没有进展的问题?

编辑:(最后一个问题)

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

 // 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)提到的:

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

但是如上所分析,循环中的两个版本应该给出相同/相似的性能。 我错过了什么?

为什么要在循环中交换?

通常,在继续之前,您希望完成您的工作,因此,您将compare_exchange_weak放入循环中,以便它尝试进行交换,直至成功(即返回true )。

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

为什么使用weak而不strong

很简单:虚假的失败并不经常发生,所以没有太大的performance。 相反,容忍这样的失败允许在一些平台上更有效地执行weak版本(与strong相比): strong必须总是检查虚假故障并掩盖它。 这很贵。

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

什么时候应该使用weak和什么时候strong

参考状态提示什么时候使用weak ,何时使用strong

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

所以答案似乎很简单:如果你只是因为虚假故障而引入一个循环,那就不要这样做。 使用strong 。 如果你有一个循环,然后使用weak

为什么在这个例子中呢?

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

另一个线程写成true时,它只是一个快速的轨道:然后我们放弃而不是试图再次写真。

关于你最后一个问题

但是如上所分析,循环中的两个版本应该给出相同/相似的性能。 我错过了什么?

维基百科 :

如果没有对所涉及的存储器位置进行并发更新,则LL / SC的实际实现并不总是成功。 两个操作之间的任何exception事件(例如上下文切换,另一个负载链接,甚至(在许多平台上)另一个加载或存储操作)都将导致存储条件虚假地失败。 如果内存总线上有任何更新广播,较旧的实现将失败。

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

既然你在这两个例子中都提供了一个明确的循环,那么对于强壮的版本来说就没有必要使用小循环了。 因此,在strong版本的例子中,检查失败是两次; 一次通过compare_exchange_strong (这是更复杂的,因为它必须区分伪故障和并发访问),一次通过你的循环。 这个昂贵的检查是不必要的,这里weak的原因会更快。

另外请注意,你的论点(LL / SC)只是实现这一点的一种可能性。 有更多的平台甚至有不同的指令集。 另外(更重要的是),请注意, std::atomic必须支持所有可能的数据types的所有操作,所以即使你声明了一千万字节的结构,你也可以使用compare_exchange 。 即使在拥有CAS的CPU上,也不能得到1000万字节的CAS,编译器会生成其他指令(可能会locking获取,然后进行非primefaces比较和交换,然后locking释放)。 现在,想一想交换一千万字节时会发生多less事情。 所以,虽然虚假错误对于8字节的交换可能非常罕见,但在这种情况下可能更常见。

简而言之,C ++为您提供了两种语义,一种是“尽力而为”( weak ),另一种是“我会确保这样做,不pipe中间会发生多less坏事”。 这些如何在各种数据types和平台上实现是完全不同的话题。 不要将自己的思维模式与特定平台上的实现联系起来; 标准库被devise为可以使用比您可能意识到的架构更多的架构。 我们可以得出的唯一一个总的结论是,保证成功通常比仅仅尝试和留下可能的失败空间更困难(因此可能需要额外的工作)。

为什么在几乎所有用途中都必须处于循环状态?

因为如果你不循环,并且它虚假地失败,你的程序没有做任何有用的事情 – 你没有更新primefaces对象,你不知道它的当前值是什么(更正:参见Cameron下面的注释)。 如果电话没有做任何有用的事情,那么做什么呢?

这是否意味着当虚假故障导致失败时我们会循环?

是。

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

在某些体系结构中, compare_exchange_weak效率更高,虚假的失败应该是相当不常见的,所以可以使用弱forms和循环来编写更高效的algorithm。

一般来说,如果您的algorithm不需要循环,那么使用强壮版本可能会更好,因为您不必担心虚假故障。 如果它仍然需要循环,即使是强壮的版本(许多algorithm都需要循环),那么在某些平台上使用弱forms可能会更有效率。

为什么在循环条件下呢?

该值可能已被另一个线程设置为true ,因此您不希望循环尝试设置它。

编辑:

但是如上所分析,循环中的两个版本应该给出相同/相似的性能。 我错过了什么?

毫无疑问,在虚假故障可能的平台上, compare_exchange_strong的实现必须更复杂,以检查虚假故障并重试。

虚弱的forms只是在虚假的失败上返回,它不会重试。

好的,所以我需要一个执行primefaces左移的函数。 我的处理器没有一个本地操作,标准库没有它的function,所以它看起来像我正在写我自己的。 开始:

 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. 其他人在我左转的时候改变了这个variables。 我的计算结果不应该应用到primefacesvariables,因为它会有效地抹去别人的写入。
  2. 我的CPU被烧毁,弱的CAS虚假地失败了。

我真的不在乎哪一个。 左移速度足够快,所以即使失败是虚假的,我也可以再做一次。

然而, 不那么快的是,强CAS需要围绕弱CAS进行强化的额外代码。 当弱CAS成功的时候,这些代码并没有太多的工作,但是如果失败的话,强大的CAS需要做一些侦测工作,以确定它是案例1还是案例2。这个侦探工作采取第二个循环的forms,有效地在我自己的循环内。 两个嵌套循环。 想象一下你的algorithm老师现在正在瞪着你。

正如我之前提到的,我不在乎这个侦探工作的结果! 无论哪种方式,我将重做CAS。 所以使用强大的CAS完全没有获得任何东西,并且使我失去了一小部分但是可衡量的效率。

换句话说,弱CAS被用来实现primefaces更新操作。 当你关心CAS的结果时使用强CAS。

我试图通过各种在线资源(例如, 这个和这个 ),C ++ 11标准以及这里给出的答案来自己回答这个问题。

相关的问题被合并(例如,“ why!expected? ”与“为什么把compare_exchange_weak()放在一个循环中 ”)并给出了相应的答案。


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

典型模式

您需要根据primefacesvariables中的值实现primefaces更新。 失败表示variables没有用我们期望的值更新,我们想重试它。 请注意, 我们并不在乎是否由于并发写入或虚假故障而失败。 但是我们确实在乎, 这是我们 做这个改变。

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

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

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

典型的模式B

这实际上是安东尼的书中提到的模式。 与模式A相反, 您希望将primefacesvariables更新一次,但不关心是谁执行的。 只要它没有更新,你再试一次。 这通常用于布尔variables。 例如,你需要实现一个状态机的触发器继续前进。 无论哪个线程拉动触发器。

 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()应该很less见。 相反,有强壮的版本正在使用的情况。 例如,

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

compare_exchange_weak在这里是compare_exchange_weak ,因为当它由于虚假失败而返回时,可能没有人占用关键部分。

饥饿的线程?

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

 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,除非primefaces对象的值与预期值不同,或者对primefaces对象有并行修改。

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

这取决于。

情况1:当两者都需要在循环内部使用时。 C ++ 11说:

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

在x86上(至less现在是这样,也许会在更多的内核被引入的时候采取类似于LL / SC的方式来performance性能),弱和强版本本质上是相同的,因为它们都归结为单指令cmpxchg 。 在compare_exchange_XXX()没有以primefaces方式实现的某些其他平台(这里意味着没有单个硬件原语存在),循环内部的弱版本可能会赢得战斗,因为强壮的人将不得不处理伪故障并相应地重试。

但,

很less,即使在循环中,我们也可能比compare_exchange_weak()更喜欢compare_exchange_strong() 。 例如,当primefacesvariables被加载并且计算出的新值被交换出来时(见上面的function() ),有很多事情要做。 如果primefacesvariables本身没有频繁变化,我们不需要重复每一次虚假失败的代价高昂的计算。 相反,我们可能希望compare_exchange_strong() “吸收”这样的失败,并且只有在失败时才会重新进行计算。

情况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()相同的操作。 更差? 这种方法无法充分利用在硬件上提供无错比较和交换的机器 。

最后,如果你循环其他的东西(例如,参见上面的“典型模式A”),那么compare_exchange_strong()也应该放在一个循环中,这会使我们回到前一种情况。