了解std :: atomic :: compare

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

为什么在循环条件下!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)也提到了这一点:

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

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


为什么要在循环中交换?

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

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

为什么使用weak而不是strong

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

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

你应该什么时候使用weakstrong

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

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

所以答案似乎很容易记住:如果仅仅因为虚假故障而不得不引入一个循环,那么不要这样做; 使用strong 。 如果你有一个循环,然后使用weak

为什么在!expected的例子中

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

当另一个线程写入为true时,它只是一个快速通道:然后我们放弃而不是试图再次写入true

关于你最后一个问题

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

维基百科:

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

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

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

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

所以,简单地说,C ++提供了两个语义,“尽力而为”一个( weak )和“我将做到这一点是肯定的,无论是不好的事情可能多少其间发生的”一个( strong )。 这些如何在各种数据类型和平台上实现是完全不同的话题。 不要将你的思维模型与特定平台上的实现联系起来; 标准库被设计为可以使用比您可能意识到的架构更多的架构。 我们可以得出的唯一一个总的结论是,保证成功通常比仅仅尝试和留下可能的失败空间更困难(因此可能需要额外的工作)。


为什么它几乎所有用途处于循环状态?

因为如果你不循环并且虚假地失败,你的程序没有做任何有用的事情 - 你没有更新原子对象,你不知道它的当前值是什么(更正:参见下面的来自Cameron的评论)。 如果通话没有做任何有用的事情,那么做什么?

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

是。

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

在某些体系结构中, compare_exchange_weak效率更高,并且虚假故障应该不太常见,因此可以使用弱形式和循环编写更高效的算法。

一般来说,如果你的算法不需要循环,那么使用强壮版本可能会更好,因为你不需要担心虚假故障。 如果即使对于强壮的版本它仍然需要循环(并且许多算法确实需要循环),那么在某些平台上使用弱形式可能会更有效。

为什么在循环条件下!expected

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

编辑:

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

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

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


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

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


为什么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_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_weak()更喜欢compare_exchange_strong() 。 例如,当原子变量被加载并且计算出的新值被交换出来时(见上面的function() ),有很多事情要做。 如果原子变量本身没有频繁变化,我们不需要重复每一次虚假故障的昂贵计算。 相反,我们可能希望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()也应该放在一个循环中,这会使我们回到前一种情况。

链接地址: http://www.djcxy.com/p/78217.html

上一篇: Understanding std::atomic::compare

下一篇: C++11 multithreading locks and atomic primitives