在多线程编程中,同步是一个关键问题,它确保了多个线程能够正确地共享数据。C++11标准引入了原子操作,使得编写可在所有标准兼容平台上运行的可移植多线程代码成为可能。其中一个重要的原子操作就是比较并交换(Compare-and-Swap,简称CAS),它是一种用于实现同步的机制。
CAS操作的基本概念是:从内存中加载一个值,将其与预期值进行比较,如果相等,则将一个预定义的期望值存储到该内存位置。重要的是,所有这些操作都是以原子方式执行的,这意味着如果在操作过程中另一个线程改变了该值,CAS操作将失败。
在C++11中,CAS操作通过以下两个函数实现:
bool compare_exchange_weak(T& expected, T desired, ...);
bool compare_exchange_strong(T& expected, T desired, ...);
如果expected
等于对象持有的实际值,则返回true
,并将desired
值写入内存。否则,expected
将被内存中的实际值覆盖,并返回false
。
然而,有一个例外:弱版本(compare_exchange_weak
)即使对象的值等于expected
,也会返回false
,并且不会与内存中的值同步。
由于“虚假失败”(spurious failure)的存在,几乎所有使用弱CAS的情况都需要在循环中进行。虚假失败是指在某些平台上,由于指令序列的实现(而非x86上的单个指令),上下文切换、同一地址(或缓存行)被另一个线程重新加载等原因,可能导致CAS操作失败。这种失败是虚假的,因为并不是对象的值(不等于expected
)导致操作失败,而是由于时间问题。相反,强版本(compare_exchange_strong
)在概念上会处理这种情况,并在任何虚假失败的情况下重试。
需要基于原子变量的值实现原子更新。失败表明变量没有更新为期望的值,希望重试。注意,并不关心失败是由于并发写入还是虚假失败。但关心的是,是自己使这个变化发生。
T expected = current.load();
do {
T desired = function(expected);
} while (!current.compare_exchange_weak(expected, desired));
一个现实世界的例子是,多个线程需要并发地向单链表添加元素。每个线程首先加载头指针,分配一个新节点,并将头指针附加到这个新节点。最后,它尝试将新节点与头节点交换。
这实际上是Anthony书中提到的模式。与模式A相反,希望原子变量只更新一次,但不在乎是谁做的更新。只要它没有更新,就再次尝试。这通常用于布尔变量。例如,需要实现一个触发器,以使状态机继续前进。哪个线程拉动触发器并不重要。
T expected = false;
// !expected: 如果expected被另一个线程设置为true,则完成!
// 否则,它将虚假失败,应该再次尝试。
while (!current.compare_exchange_weak(expected, true) && !expected);
注意,通常不能使用这种模式来实现互斥锁。否则,可能会有多个线程同时进入临界区。
这取决于情况。
C++11标准指出:
当compare-and-exchange在循环中时,弱版本在某些平台上将提供更好的性能。
在x86(至少目前是这样)上,弱版本和强版本本质上是相同的,因为它们都可以归结为单个指令cmpxchg
。在某些平台上,如果compare_exchange_XXX()
不是原子地实现的(这里意味着没有单个硬件原语存在),循环中的弱版本可能会胜出,因为强版本将不得不处理虚假失败并相应地重试。
但是,很少情况下,可能更倾向于在循环中使用compare_exchange_strong()
而不是compare_exchange_weak()
。例如,当原子变量加载和计算新值交换之间有很多工作要做时(见上面的function()
)。如果原子变量本身不经常变化,不需要为每次虚假失败重复昂贵的计算。相反,可能希望compare_exchange_strong()
“吸收”这些失败,只在它因真实值变化而失败时重复计算。
C++11标准还指出:
当弱compare-and-exchange需要循环而强版本不需要时,强版本更可取。
这通常是当循环只是为了消除弱版本的虚假失败时的情况。重试,直到交换成功或由于并发写入而失败。
T expected = false;
// !expected: 如果它虚假失败,应该再次尝试。
while (!current.compare_exchange_weak(expected, true) && !expected);