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

601

积分

0

好友

77

主题
发表于 前天 03:36 | 查看: 5| 回复: 0

C++原子操作是无锁同步的核心机制,用于多线程环境下对共享数据的不可中断操作,能够避免竞争条件,是替代传统互斥锁(std::mutex)实现更高效并发控制的现代基石。原子操作由 <atomic> 头文件提供,覆盖基本数据类型(如 intbool)和自定义类型(C++20起)。

竞争条件与原子性

竞争条件

当多个线程并发访问共享数据资源,且其中至少一个线程执行写操作时,若相关操作未实施适当的同步机制,则程序的最终执行结果将取决于线程调度的具体顺序,从而可能引发不可预期的逻辑错误或数据不一致问题。这本质上是并发编程中需要解决的核心问题之一。

示例(非原子操作的竞态)

#include <iostream>
#include <thread>

// 共享变量
int count = 0;

void increment(){
    for (int i = 0; i < 100000; ++i)
    {
        // 非原子操作:读→加1→写,三步可被中断
        // 此处的汇编可以参考下文的图片。
        count++;
    }
}

int main(){
    std::thread t1(increment);
    std::thread t2(increment);
    t1.join();
    t2.join();

    // 结果大概率 < 200000
    std::cout << count << std::endl;
    return 0;
}

说明:count++ 可以拆解为读、加1、写三个可被中断的步骤,当多个线程交错执行时,就会导致数据错误。

图片

原子操作

原子操作即其执行过程不可被中断,要么完全执行,要么完全不执行,没有中间状态。 C++原子操作通过 std::atomic 模板实现,底层依赖CPU的特定指令(如 lock xaddcmpxchg),无需程序员显式加锁。

基本用法:std::atomic

std::atomic 是模板类,支持绝大多数基本数据类型(boolcharintlong、指针等),C++20起支持自定义平凡类型。

核心接口(以 std::atomic<int> 为例)

图片

示例(修复竞态条件)
#include <atomic>
#include <iostream>
#include <thread>

// 共享变量
std::atomic<int> count(0);

void increment(){
    for (int i = 0; i < 100000; ++i)
    {
        // 原子操作
        count++;
    }
}

int main(){
    std::thread t1(increment);
    std::thread t2(increment);
    t1.join();
    t2.join();

    // 结果稳定为 200000
    std::cout << count << std::endl;
    return 0;
}

说明:上述代码对应的汇编使用“lock xadd”指令实现原子加,其锁定的范围比传统互斥锁更小,因此效率更高。 图片 图片

内存序问题

原子操作的核心目标是保证多线程下共享变量“读-写”操作的原子性,解决竞态条件;而多核CPU架构下的缓存一致性协议(如MESI)是实现的硬件基础。编译器和处理器的指令重排序则会引发线程间内存可见性问题。通过标准化的内存序(std::memory_order)机制,可以精准约束原子操作的内存可见性与执行顺序,在确保正确性的前提下最大化性能。

内存序相关概念说明

图片

核心内存序枚举值(约束强度从高到低)

图片

示例(生产者-消费者)
#include <atomic>
#include <iostream>
#include <thread>

std::atomic<bool> ready(false);
std::atomic<int>  data(0);

// 生产者线程:写数据,释放内存序
void Producer(){
    // 步骤1:松散序写入data,仅保证原子性
    data.store(42, std::memory_order_relaxed);

    // 步骤2:释放序写入ready
    // 保证本线程中,步骤1的写入对后续用acquire读取此ready的线程可见
    ready.store(true, std::memory_order_release);
}

// 消费者线程:读数据,获取内存序
void Consumer(){
    // 步骤1:循环用acquire序读取ready
    // 一旦读到true,保证能看到生产者release之前的所有写入
    while (!ready.load(std::memory_order_acquire))
    {
        // 自旋等待
    }

    // 步骤2:松散序读取data,此时必然能看到data=42
    std::cout << data.load(std::memory_order_relaxed) << std::endl;
}

int main(){
    std::thread t1(Producer);
    std::thread t2(Consumer);
    t1.join();
    t2.join();
    return 0;
}

关键细节说明

  • memory_order_acquire(获取序)与生产者的 release 形成配对,建立了同步关系,保证了data写入的可见性,并禁止了data.load被重排到ready.load之前。
  • 循环空等(自旋)在此仅为简化示例,实际应用中可考虑使用 std::condition_variable 以减少CPU占用。

比较并交换(CAS):无锁编程核心

CAS是原子操作中的关键工具,用于实现无锁的条件更新。其逻辑是:若原子变量的当前值等于预期值(expected),则将其原子性地替换为目标值(desired)并返回 true;否则不修改,返回 false

接口简介
  • compare_exchange_weak(expected, desired, mem_order):弱版本,可能发生“伪失败”(值匹配但返回false),通常需在循环中调用,性能更高。
  • compare_exchange_strong(expected, desired, mem_order):强版本,无伪失败,但性能略低。
compare_exchange_weak的返回值规则

图片

  • 返回 true:CAS成功,原子变量已更新为desired
  • 返回 false:可能是真实失败(值不相等),也可能是伪失败(值相等)。失败时,expected 通常会被更新为原子变量的实际当前值。
  • 实际使用中,weak版本几乎总要配合循环。

使用示例

std::atomic<int> ai{1}; // 初始值1

int expected = 1;
const int desired = 2;

// 循环使用weak版本,抵消伪失败
while (!ai.compare_exchange_weak(expected, desired)) {
    // 失败时,expected已被更新为当前实际值,下次循环用新值重试
}
// 循环退出时,ai的值一定是2
compare_exchange_weakcompare_exchange_strong的返回值对比

图片

无锁编程示例(无锁栈的核心逻辑)

这个例子展示了如何利用CAS实现一个基础的无锁数据结构

#include <atomic>
#include <iostream>

struct Node{
    int   value;
    Node* next;
};

// 栈顶指针(原子指针)
std::atomic<Node*> head(nullptr);

// 无锁入栈
void push(int val){
    Node* newNode = new Node{val, nullptr};
    Node* oldHead = head.load(std::memory_order_relaxed);

    // 循环CAS:直到替换栈顶成功
    do {
        // 新节点指向旧栈顶
        newNode->next = oldHead;
    } while (!head.compare_exchange_weak(
        oldHead, newNode,
        // 成功时:释放内存序
        std::memory_order_release,
        // 失败时:松散序
        std::memory_order_relaxed));
}

int main(){
    push(10);
    push(20);
    Node* top = head.load();
    // 输出 20
    std::cout << top->value << std::endl;
    return 0;
}
原子类型的限制
  • 支持的类型:默认支持标量类型,C++20支持平凡可复制类型。
  • 不支持的操作:原子类型不支持重载的复杂运算符(+=等除外),需通过 fetch_* 系列接口实现。
  • 大小与锁:超过系统指针大小的类型可能由库内部加锁实现,可通过 std::atomic<T>::is_lock_free() 成员函数查询是否真正无锁。

原子操作与互斥锁对比

图片

技术选型原则

  1. 简单共享变量(计数器、标志位)优先使用原子操作。
  2. 多步操作或复杂的临界区(需修改多个关联变量)应使用互斥锁。
  3. 高性能无锁设计,可基于CAS组合实现原子操作。

高级用法:原子标志与内存屏障

最轻量的原子类型:std::atomic_flag

仅支持 test_and_set()(置1并返回旧值)和 clear()(置0),是唯一保证在所有平台上都无锁的原子类型。

示例(简易自旋锁)

#include <atomic>
#include <iostream>
#include <thread>

// 初始化必须用该宏
std::atomic_flag lock = ATOMIC_FLAG_INIT;

void CriticalSection(int id){
    // 自旋等待,直到获取锁(test_and_set 返回 false)
    while (lock.test_and_set(std::memory_order_acquire))
    {
    }

    std::cout << "Thread " << id << " enter critical section" << std::endl;

    // ... 临界区操作 ...

    // 释放锁
    lock.clear(std::memory_order_release);
}

int main(){
    std::thread t1(CriticalSection, 1);
    std::thread t2(CriticalSection, 2);
    t1.join();
    t2.join();
    return 0;
}
内存屏障:std::atomic_thread_fence

内存屏障是用于约束编译器及CPU对内存操作重排序、并保证可见性的底层指令。std::atomic_thread_fence 提供了一种独立于原子变量的显式屏障控制。

#include <atomic>
#include <iostream>
#include <thread>

std::atomic<bool> flag(false);
int               data = 0;

void Producer(){
    data = 42;
    // 释放屏障:保证此前的写操作对后续获取屏障可见
    std::atomic_thread_fence(std::memory_order_release);
    flag.store(true, std::memory_order_relaxed);
}

void Consumer(){
    while (!flag.load(std::memory_order_relaxed))
    {
    }
    // 获取屏障:保证能看到释放屏障之前的所有写操作
    std::atomic_thread_fence(std::memory_order_acquire);
    // 稳定输出 42
    std::cout << data << std::endl;
}

int main(){
    std::thread t1(Producer);
    std::thread t2(Consumer);
    t1.join();
    t2.join();
    return 0;
}

常见坑与避坑指南

  1. 误用原子操作(非原子的复合操作)

    std::atomic<int> a(0);
    // 错误:非原子!拆解为 load(a) → +1 → store(a)
    a = a + 1;
    // 正确做法
    a.fetch_add(1); // 或 a++;
  2. 内存序过度使用 seq_cst memory_order_seq_cst 是默认值,但性能开销最大。仅在需要严格的全局顺序一致性时使用,其他场景应优先考虑 acquire/releaserelaxed

  3. CAS伪失败未处理 compare_exchange_weak 可能伪失败,必须循环调用。

    int expected = a.load();
    // 错误:无循环,可能因伪失败而失败
    a.compare_exchange_weak(expected, 10);
    // 正确:循环直到成功
    while (!a.compare_exchange_weak(expected, 10)) {}
  4. 原子类型的拷贝/赋值限制 std::atomic 对象不可拷贝或移动。

    std::atomic<int> a(5);
    // 编译错误:拷贝构造被删除
    // std::atomic<int> b = a;
    // 正确做法:通过 load/store 传递值
    std::atomic<int> b;
    b.store(a.load());

总结

  • 核心价值:原子操作是构建高效无锁并发的基础,相比互斥锁开销更低,适用于简单共享变量的同步。
  • 关键要点
    • 优先使用 std::atomic 替代“裸变量+锁”。
    • 根据场景合理选择内存序,避免滥用 memory_order_seq_cst
    • 使用CAS(特别是weak版本)时,务必循环处理伪失败。
  • 适用场景:计数器、标志位、无锁数据结构(栈/队列)、线程间简单同步。
  • 边界注意:复杂临界区仍需互斥锁;对于自定义或大类型,使用前可通过 is_lock_free() 确认其是否真为无锁实现,这对于理解多线程程序性能至关重要。



上一篇:Java反序列化漏洞黑盒挖掘实战:JDBC、XStream等组件检测方法
下一篇:PyTorch实战:20维到1维的MLP建模与低误差回归任务解析
您需要登录后才可以回帖 登录 | 立即注册

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

GMT+8, 2025-12-11 04:56 , Processed in 0.093273 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

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