找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3473

积分

0

好友

474

主题
发表于 18 小时前 | 查看: 2| 回复: 0

一、从无锁编程谈起

在前文探讨多线程编程时,我们提及了基于CAS的无锁编程技术。实现无锁编程的一个重要前提,是标准库(以及底层系统)提供的原子操作类型,它们本质上是对硬件比较-交换(Compare-And-Swap)指令的封装与向上抽象。在C++标准库中,提供了两个核心接口来实现CAS操作:compare_exchange_strongcompare_exchange_weak

需要注意的是,不同平台或编程语言中,CAS的接口和底层实现机制可能存在差异,这一点必须明确。

二、强与弱的比较交换

compare_exchange_strongcompare_exchange_weak,从字面意思就很好理解。强比较交换是严格的比较,不会出现伪失败,性能相对稍差;弱比较交换则更为宽容,允许出现伪失败,但性能通常稍好。这种设计是必要的,不同的应用场景需要匹配合适的工具,而不是强行用一把钥匙开所有的锁。

两者的核心区别在于:强比较交换只有在比较值真正不同时才会失败;而弱比较交换即使在比较值相同时,也可能因故失败。

先看看它们在C++中的函数声明:

bool compare_exchange_weak( T& expected, T desired,
                           std::memory_order success,
                           std::memory_order failure );

这里有一个容易让非母语者困惑的地方。我们先明确这个函数的行为逻辑:

  1. 比较当前原子变量的值是否等于调用者提供的预期值 (expected)。
  2. 如果相等,则将原子变量的值设置为调用者提供的新值 (desired)。
  3. 如果不相等,则将原子变量的当前值写入 expected 参数,并返回 false

这里出现了 expecteddesired 两个与“期望”相关的参数,哪个才是步骤1中的“预期值”呢?查看接口说明便能明白:

expected - pointer to the value expected to be found in the atomic object
desired  - the value to store in the atomic object if it is as expected

从英文语义来看,expected 是一种客观的预测或合理的期望,即它代表我们“预期”原子对象中应该存在的值;而 desired 更强调主观愿望,代表在符合预期的情况下,我们“期望”存储进去的新值。结合上面的英文说明,expected 是调用者提供的期望值,而 desired 是匹配成功后的目标存储值。

尽管不同平台存在细微差别,但在软件层面通常会进行“透明化”处理,上层开发者无需感知。目前开发者接触的主要环境还是x86平台。ARM平台主要用于移动端,在这些场景下,直接使用CAS的机会相对较少。

在x86平台上,提供了 CMPXCHG 指令,这是一个原子操作指令。原子操作意味着该指令的执行过程是不可分割的。因此,在这种底层实现上,强弱两个版本的差异不大。这不禁让人联想到x86的内存序问题,确实,内存序也影响着CAS指令的行为。常见的内存序及其对编译器和处理器优化的影响如下:

memory_order_seq_cst:全局顺序一致,要求最严格,对性能影响最小
memory_order_acq_rel:获取-释放语义,要求中等,影响中等
memory_order_relaxed:仅保证原子性,要求最低,对性能影响最大(允许更多优化)

而在AMD等采用LL/SC(Load-Linked/Store-Conditional)机制且默认采用弱内存序的平台上,由于指令处理的并行行为,就可能发生伪失败。这属于硬件架构的固有特性,软件层面难以完全规避。

一般来说,在非循环场景或严格要求成功率的场景下,推荐使用 compare_exchange_strong;在循环(例如自旋锁实现)中,为了性能考虑,可以使用 compare_exchange_weak。在x86平台上,二者性能几乎无差别;但在ARM等平台上,出于性能考虑,常选择 compare_exchange_weak 并配合循环处理可能的伪失败。

三、伪失败、ABA问题与活锁

在CAS应用场景中,ABA问题和活锁是经常被讨论的难点(关于多核CPU编程中的ABA问题与死锁/活锁,可查阅相关文章)。这里我们重点分析一下“伪失败”。

所谓伪失败,即 spurious failure,意思就是“本不应该失败的失败”。在应该成功赋值的情况下,因为伪失败被判定为失败,导致预期操作没有执行。伪失败是 compare_exchange_weak 的典型特征,用于与 compare_exchange_strong 进行明确区分(这种设计思路有点像布隆过滤器)。你可以通俗地将其理解为“人有失手,马有失蹄”。

伪失败的产生原因主要有:

  1. 硬件架构:主要是基于LL/SC架构的平台,如PowerPC、MIPS、RISC-V等。
  2. 缓存一致性:多核处理器中的缓存一致性问题。
  3. 中断与异常:执行过程中被中断或异常事件打断。
  4. 多线程访问冲突:多个线程几乎同时对同一内存位置进行访问操作,这可以看作是硬件冲突在软件层的映射。

从以上分析可以看出,伪失败与硬件关系密切。像x86这类平台,基本无需考虑此问题。

四、应用与示例代码

下面看看cppreference上提供的一个简单例程:

#include <atomic>
#include <iostream>

std::atomic<int>  ai;

int tst_val = 4;
int new_val = 5;
bool exchanged = false;

void valsout()
{
    std::cout << "ai= " << ai
              << "  tst_val= " << tst_val
              << "  new_val= " << new_val
              << "  exchanged= " << std::boolalpha << exchanged
              << "\n";
}

int main()
{
    ai = 3;
    valsout();

    // tst_val != ai   ==>  tst_val is modified
    exchanged = ai.compare_exchange_strong(tst_val, new_val);
    valsout();

    // tst_val == ai   ==>  ai is modified
    exchanged = ai.compare_exchange_strong(tst_val, new_val);
    valsout();

    return 0;
}

运行结果如下:

ai= 3  tst_val= 4  new_val= 5  exchanged= false
ai= 3  tst_val= 3  new_val= 5  exchanged= false
ai= 5  tst_val= 3  new_val= 5  exchanged= true

如果你对CAS相关的实际应用感兴趣,可以在GitHub或其他技术网站上找到大量更复杂的算法实现和锁设计示例,这里就不再赘述。

五、总结

从一个技术点的不同层面去剖析,往往能获得全新的认知。这就像“盲人摸象”的故事,每个人听来都觉得简单可笑。但在实际的技术开发工作中,我们却常常不自觉地陷入类似的片面认知。随着技术视野的不断拓宽和经验的持续积累,你会越发认识到自身知识的局限性。这恰恰印证了哲学上所说的“波浪式前进,螺旋式上升”的发展规律。深入研究C/C++标准库的实现细节,是提升对并发编程理解的重要途径。




上一篇:AI原生软件将如何颠覆传统App开发与分发模式?
下一篇:SAP AI战略演进:从嵌入式助手到无应用(No-App)ERP的颠覆之路
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-25 21:11 , Processed in 0.368512 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表