1. 从CAS到循环:为什么compare_exchange_weak是性能利器
如果你写过C++多线程程序,特别是用过无锁数据结构,那你肯定绕不开一个东西:CAS。CAS,全称Compare-And-Swap,中文叫比较并交换,它是现代并发编程的基石之一。简单来说,CAS就是一个“乐观锁”操作:我先看看内存里的值是不是我期望的那个,如果是,我就把它改成新值;如果不是,那说明在我“看”和“改”之间,已经有别的线程捷足先登了,我就放弃这次修改。
C++11标准库在 <atomic> 头文件里,为我们提供了两个实现CAS操作的函数:compare_exchange_strong 和 compare_exchange_weak。很多刚接触的朋友会疑惑,这俩名字听着差不多,到底有啥区别?原始文章里提到了一句:“compare_exchange_weak比compare_exchange_strong的性能更高一些,常在循环中使用。” 这句话点出了关键,但背后的门道可深了。今天,我就结合自己这些年踩过的坑和调优的经验,跟你深入聊聊,为什么在循环场景下,compare_exchange_weak 往往是那个更高效、更“对味儿”的选择。
咱们先打个比方。compare_exchange_strong 就像一个做事一板一眼、追求绝对成功的管家。你交代他:“去把客厅的花瓶换成新的,但前提是客厅里现在摆的还是旧花瓶。” 这个管家会严格执行,只要他发现客厅里不是旧花瓶(被其他人换过了),他就会明确告诉你“任务失败”,并且保证这个“失败”的结果是真实可靠的,绝不会因为自己眼花或者手滑而误报。而 compare_exchange_weak 呢,更像一个手脚麻利但偶尔会“眼花了”的助手。他同样去执行换花瓶的任务,但在极少数情况下,即使客厅里摆的确实是旧花瓶,他也可能因为一些外部干扰(比如CPU指令执行时的细微时序问题)而“觉得”自己没换成,然后告诉你失败了。这种失败是“虚假失败”,英文叫 spurious failure。
听到“虚假失败”,你可能觉得这助手不靠谱,怎么能用他呢?别急,这正是 compare_exchange_weak 的精妙之处。它用偶尔可以接受的、无伤大雅的“虚假失败”,换来了在特定场景下——尤其是循环——更高的执行效率。这个效率提升,在高度竞争的多线程环境下,带来的性能收益是相当可观的。接下来,我们就钻进代码和原理里,看看它到底是怎么工作的。
2. 庖丁解牛:深入理解weak与strong的本质区别
要弄明白为什么weak版本在循环里更高效,我们得先搞清楚这两个函数行为上的核心差异。这个差异,主要就体现在对“失败”的处理上。
compare_exchange_strong 保证的是 强一致性。它的语义非常明确:只有当原子变量的当前值确实等于我提供的期望值时,交换才会成功;否则,交换一定失败。这个“失败”是板上钉钉的,不存在任何模棱两可的情况。为了实现这个强保证,在一些底层硬件架构(比如某些ARM或早期x86)上,编译器可能需要生成额外的指令来确保这个操作的原子性和一致性是“强”的,这可能会引入一点点额外的开销。
而 compare_exchange_weak 提供的是 弱一致性。它允许“虚假失败”:即使当前值等于期望值,这个函数也可能莫名其妙地返回失败。听起来很坑对吧?但关键在于,它只允许在应该成功的时候失败,绝不允许在应该失败的时候成功。也就是说,如果当前值不等于期望值,它100%返回失败;只有当前值等于期望值时,它才可能因为某些与值无关的底层原因(例如CPU缓存一致性协议下的冲突、内存顺序约束等)而“抽风”一下,返回失败。这种失败是安全的,因为它没有改变任何数据(交换没发生),只是让你重试一次。
那么,这个“虚假失败”的根源在哪呢?这得深入到CPU指令层。在很多平台上,C++的原子操作最终会映射到特定的CPU指令,比如x86的 CMPXCHG。compare_exchange_strong 通常可以直接、完整地利用这条指令的语义。而 compare_exchange_weak 则可能在某些架构上,为了实现更高的性能或兼容性,采用了略有不同的实现方式,这偶尔会引发与值比较无关的失败。你可以把它理解为一种性能与严格性之间的权衡。
这里有一个非常关键的认知:compare_exchange_weak 的“弱”并不是指它的功能弱,或者结果不可靠。它的结果(成功或失败)在业务逻辑上依然是完全可靠的


1718

被折叠的 条评论
为什么被折叠?



