C/C++——atomic小记

CAS操作

CAS操作就是"compare_and_swap",比较和交换。这是实现atomic原子操作的基石,在一些平台下,CAS本身就是一个原子指令,因此是原子的;在另一些平台下,CAS是硬件模拟原子的,不止由一个指令组成。


CAS的weak和strong

**CAS在C++中有两种接口:****compare_exchange_weak 和 compare_exchange_strong ,****一般我们说weak是不安全但是高效的、strong是安全但是不效的。**只有在多指令CAS下才有weak和strong这个说法,如果是单指令CAS不会有这种区别。

在多指令CAS中,操作分为三步:

cpp 复制代码
第一步:LL (Load-Linked) - 读取值,并"标记"这个内存地址
LL R1, [内存地址]        ; R1 = 100,开始监视这个地址

第二步:比较
CMP R1, 期望值           ; 100 == 100?是的!

第三步:SC (Store-Conditional) - 条件存储
SC R2, [内存地址]        ; 尝试写入新值
                         ; 但!如果这个地址被"打扰"过,写入失败!
                         ; 返回 0(失败)

**看懂了吗,也就是说,在第一步而第三步中间可能有其他线程会直接修改变量值,因为缓存一致性,对于本线程来说,这个变量的寄存器值失效了,所以无法写入,所以执行失败。**这种判断相等之后依旧执行失败的现象叫做"伪失败"(正常失败是在第一步的时候比较失败,这是正常的,但是伪失败是在我们已经赋予CAS原子性语义的情况下中间操作失败,这是逻辑上的错误)

所以说,如果我们直接使用compare_exchange_weak,可能会出现伪失败,最终导致数据不一致的情况。但是这是可以避免的:

cpp 复制代码
  // weak 版本 - 可能伪失败
    while (!value.compare_exchange_weak(expected, newvalue)) {
        // 如果失败(包括伪失败),expected 会被更新为当前值
        // 循环重试
    }

而compare_exchange_strong做了什么呢?

cpp 复制代码
// strong 的内部实现(伪代码)
bool compare_exchange_strong(expected, desired) {
    // 内部循环调用 weak 版本
    while (true) {
        bool success = compare_exchange_weak(expected, desired);
        
        if (success) {
            return true;  // 成功了
        }
        
        // 失败有两种情况:
        if (expected != actual_value) {
            return false;  // 真失败:值确实变了
        }
        
        // 伪失败:值没变,继续循环重试
        // expected 已经被更新为当前值(还是原值)
        // 继续下一次尝试
    }
}

实际上,strong也是循环重试,而compare_exchange_strong和compare_exchange_weak的性能差异就体现在多出来的两个条件判断上。

strong为了维护CAS原子性的语义,不能把两种失败都直接返回false,因此在其内部做了判断区分。


atomic无锁头插链表

cpp 复制代码
typedef struct node 
{
    node(int val,node* ne)
    {
	    _val = val;
	    _next = ne;
    }
    struct node* _next;
    int _val = 0;
}Node;

atomic<Node*> head(nullptr);

void func(int i)
{
    Node* old = head.load();
    Node* newnode = new Node(i,nullptr);

    do 
    {
	    newnode->_next = old;
    } while (!head.compare_exchange_weak(old, newnode));
   
}

int main() 
{
    int k = 0;
    vector<thread> arr;
    for (int i = 0;i<10;i++) 
    {
	    arr.push_back(thread(func, k++));
    }

    for ( auto & e:arr) 
    {
	    e.join();
    }

    Node* cur = head.load();
    while(cur!=nullptr) 
    {
	    cout << cur->_val <<" ";
	    cur = cur->_next;
    }
    cout << endl;
}

atomic内存序

一些时候,为了优化程序效率,会把指令顺序重排。对于指令乱序,这块需要注意的是:编译器只需要保证在单线程环境下,执行的结果最终一致就可以了,所以,指令乱序在单线程环境下完全是允许的。但是,编译器并不知道哪些数据是在线程间共享,而且是有可能会被修改的,这些是需要开发人员去保证的。

而atomic,这个类天生就是为多线程而生,因此一旦你使用atomic,那么就需要考虑内存序问题,内存序就是限制本来可以重排的命令之间的顺序以保证多线程并发安全(锁在内部其实也保证了内存序,只是它封装起来了我们看不到)

atomic的操作,比如说load,比如说compare_exchange_weak,都可以设置内存序,以防止出错。

1. memory_order_relaxed(最宽松)

  • 语义:仅保证原子操作的原子性,不提供任何同步或顺序约束

  • 重排限制:无,编译器和CPU可任意重排该操作周围的指令

示例:

cpp 复制代码
std::atomic<int> x(0);
x.store(42, std::memory_order_relaxed); // 仅保证原⼦性

2.memory_order_acquire(获取语义)

  • 语义:这个内存序只可以在包含读操作的atomic操作中使用(比如load,比如exchange),读操作之后,可能本线程就要能执行了,这个内存序保证读操作之后的所有读写操作都一定是在可以执行之后才执行的,不能因为重排而打乱导致并发问题。

  • 重排限制:防止后续操作(读或写)被提到该操作之前执行

  1. memory_order_release(释放语义)
  • 语义:这个内存序只可以在包含写操作的atomic操作中使用(比如store,exchange),写操作之后,其他线程就会被唤醒,这个内存序保证写操作之前的所有读写操作都一定是被执行的,防止被唤醒的线程访问出错。

  • 重排限制:防止先前操作(读或写)被延迟到该操作之后执行

示例:

​​​

cpp 复制代码
std::atomic<bool> flag(false);
int data = 0

// 线程 1
data = 42;
flag.store(true, std::memory_order_release); // 保证 data = 42 在 flag=true 之前可⻅

// 线程 2
while (!flag.load(std::memory_order_acquire)) {}
std::cout << data; // 保证看到 data = 42

上面是一个线程修改另一个线程负责打印的例子:

  • 如果没有 std::memory_order_release ,那么有可能flag的设置会在data赋值之前,这样可能会造成线程2打印出0的错误结果。
  • 如果没有 std::memory_order_acquire ,编译器可能会认为,while循环和打印交换没什么影响****因为在线程2的函数中data没有被修改,因此出现错误。

4. memory_order_acq_rel(获取-释放混合)

  • 语义:同时具有 acquirerelease 的语义,一般用于同时进行读写操作的原子操作(比如fetch_add)。

  • 重排限制:既防止前面的操作被移到之后,也防止后面的操作被移到之前(双向限制)

  • 示例:

    cpp 复制代码
    std::atomic<int> counter(0);
    int old = counter.fetch_add(1, std::memory_order_acq_rel);
    // 既保证之前操作不会被重排到后面,也保证之后操作不会被重排到前面

5. memory_order_seq_cst(顺序一致性,默认)

  • 语义:所有线程看到的内存操作顺序完全一致(全局顺序一致性)

  • 重排限制:最强限制,完全禁止所有相关重排,并提供全局同步点

  • 示例:

    cpp 复制代码
    std::atomic<int> x(0);
    x.store(42, std::memory_order_seq_cst);  // 默认模式
    int value = x.load(std::memory_order_seq_cst);
    // 所有线程保证看到相同顺序
相关推荐
千谦阙听9 小时前
C++类和对象(中):默认成员函数、构造与析构、拷贝构造、运算符重载
开发语言·c++·学习
weixin_307779139 小时前
一维无粘 Burgers 方程的激波形成问题:MacCormack 格式求解
c++·算法·matlab
HugoStudio_SWAN10 小时前
洛谷 B4500 / B4449 / B3843 凯撒密码、密码强度与密码合规——加密与安全的三道门
c++·学习·程序人生·算法·安全
雾里看山10 小时前
源码目录、构建目录与 out-of-source build
c++·软件构建·cmake
渡我白衣12 小时前
并查集:基础认识与模拟实现
android·java·javascript·数据结构·c++·算法·并查集
如意猴12 小时前
【C++】001--C++入门(1)
开发语言·c++
hetao173383713 小时前
2026-09-01~09-04 hetao1733837 的刷题记录
c++·算法
HugoStudio_SWAN15 小时前
洛谷 P1420 / P1179 / B4262 最长连号、数字统计与词频统计——统计的三种面孔
c++·学习·程序人生·算法
raindayinrain16 小时前
c++并发
c++