C++中的atomic

在C++多线程编程中,std::atomic(原子操作)是实现无锁编程(Lock-Free)和轻量级同步 的核心机制。它定义在 <atomic> 头文件中。


1. 为什么要用 Atomic?

在多线程环境下,简单的 i++ 操作其实包含三个步骤:

  1. Read : 从内存读取 i 的值到寄存器。
  2. Modify: 在寄存器中将值加 1。
  3. Write: 将新值写回内存。

如果没有加锁或使用原子变量,两个线程同时执行 i++,可能会导致所谓的数据竞争(Data Race),也就是丢失更新。

Atomic 的作用: 保证上述 Read-Modify-Write 过程合并为一个不可打断的硬件指令(如 x86 的 lock add),从而保证线程安全。


2. 基础用法

最常用的场景是计数器或标志位。

复制代码
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>

// 定义一个原子整型,初始化为0
std::atomic<int> counter(0); 

void work() {
    for (int i = 0; i < 10000; ++i) {
        counter++; // 原子自增,等价于 counter.fetch_add(1)
    }
}

int main() {
    std::thread t1(work);
    std::thread t2(work);

    t1.join();
    t2.join();

    // 结果一定是 20000,如果是普通 int 则结果不确定
    std::cout << "Result: " << counter << std::endl; 
    return 0;
}

注意: 虽然 counter++ 是原子的,但 counter = counter + 1不是原子的!因为它把读取和写入分成了两个独立的语句。


3. 核心成员函数 (API)

std::atomic<T> 提供了比简单的 ++ 更精细的控制:

|-------------------------|------------------------|-------------------|
| 函数名 | 描述 | 对应操作 |
| load() | 读取原子变量的值 | Read |
| store(val) | 修改原子变量的值 | Write |
| exchange(val) | 交换值:写入新值,并返回旧值 | Read-Modify-Write |
| fetch_add(val) | 加法:加上 val,并返回加之前的值 | Read-Modify-Write |
| fetch_sub(val) | 减法:减去 val,并返回减之前的值 | Read-Modify-Write |
| compare_exchange_* | CAS 操作(重点) | Compare-And-Swap |


4. CAS 操作 (Compare And Swap)

这是无锁编程的基石。它的逻辑是:"我认为当前内存里的值应该是 A(预期值),如果是,就把他改成 B(新值);如果不是,告诉我当前内存里到底是多少。"

C++ 提供了两个版本:

  • compare_exchange_strong
  • compare_exchange_weak (允许伪失败,效率更高,常用于循环中)

CAS 经典代码模板(实现一个无锁的原子更新):

复制代码
void atomic_multiply(std::atomic<int>& current_val, int multiplier) {
    int expected = current_val.load(); // 1. 读取当前值作为"预期值"
    int desired;
    
    do {
        desired = expected * multiplier; // 2. 计算想要写入的新值
        
        // 3. 尝试更新:
        // 如果 current_val 依然等于 expected,则 current_val = desired,返回 true。
        // 如果 current_val 不等于 expected(被其他线程改了),
        // 则 expected = current_val (更新预期值为当前最新值),返回 false。
    } while (!current_val.compare_exchange_weak(expected, desired)); 
}

这个循环被称为 CAS Loop 。它体现了乐观锁的思想:假设没人跟我抢,如果真有人抢了,我就重试。


5. 内存顺序 (Memory Order) - 进阶必读

std::atomic 的操作函数通常有一个可选参数 std::memory_order。这是 C++ 原子操作最难理解但也最强大的部分。它控制编译器和 CPU 如何对指令进行重排序

默认情况下,所有原子操作都使用 std::memory_order_seq_cst(顺序一致性),这是最安全但性能消耗最大的模式。

常见的内存序:

  1. memory_order_relaxed**(松散序)**
    • 只保证原子性,不保证顺序。
    • 用途: 纯粹的计数器(如统计网页访问量),不涉及线程间的同步逻辑。
  1. memory_order_acquire**(获取)** & memory_order_release**(释放)**
    • 成对使用,建立同步屏障。
    • Release: 之前的写操作不允许重排到 Release 之后(保证写完了才能发信号)。
    • Acquire: 之后的读操作不允许重排到 Acquire 之前(保证收到信号后才开始读)。
    • 用途: 互斥锁的底层实现、生产者-消费者模型。
  1. memory_order_seq_cst**(顺序一致性)**
    • 默认值。 保证所有线程看到的指令执行顺序是一致的。
    • 用途: 对安全性要求高、性能不那么敏感的场景。

6. std::atomic 与 std::mutex 的对比

|----------|-----------------------------|---------------------|
| 特性 | std::atomic | std::mutex |
| 机制 | 硬件指令支持 (CPU Level) | 操作系统内核调度 (OS Level) |
| 阻塞 | 非阻塞 (Lock-Free),线程不会挂起 | 阻塞,线程会挂起等待唤醒 |
| 开销 | 极小,主要在 CPU 流水线和缓存 | 较大,涉及上下文切换 |
| 适用场景 | 简单数据类型 (int, ptr, bool) 的同步 | 复杂逻辑、大块代码段的同步 |
| 复杂性 | 需要理解内存序,容易写出 Bug | 逻辑简单清晰,不易出错 |

7.automic 实现无锁栈

复制代码
// 实现一个无锁的线程安全栈

#include <iostream>
#include <atomic>
#include <vector>
#include <thread>
#include<unistd.h>
#include<chrono>

template <typename T>
struct Node
{
    T _data;
    Node *_next;

    Node(const T &data) : _data(data), _next(nullptr)
    {
    }
};

template <typename T>
class LockFreeStack
{
    using node = Node<T>;
    using atomic_head = std::atomic<node *>;

private:
    atomic_head _head;

public:
    LockFreeStack() : _head(nullptr)
    {
    }

    // 入栈操作,头插
    void push(const T &val)
    {
        node *old_head = _head.load();

        node *new_node = new node(val);
        new_node->_next = old_head;

        while (!_head.compare_exchange_weak(old_head, new_node))
        {
            new_node->_next = old_head;
        }
    }

    // 出栈操作,去除头部节点
    bool pop(T &result)
    {
        
        node *old_head = _head.load();

        // 如果指定的头结点和现有的不同则将现有的给old_head,再进行一次
        while (old_head && !_head.compare_exchange_weak(old_head, old_head->_next))
        {
        }

        // 如果旧指针头部是空节点,证明栈为空
        if (old_head == nullptr)
        {
            return false;
        }

        result = old_head->_data;

        //std::cout<<result<<std::endl;

        delete old_head;
        return true;
    }
};

int main()
{
    LockFreeStack<int> st;

    std::vector<std::thread> threads;

    for (int i = 0; i < 10; i++)
    {
        if (i < 5)
        {
            threads.push_back(std::thread([&]()
                                          { st.push(i + 10); }));
        }
        else
        {
           
            int count = 0;
            // 这里可能会报错
            threads.push_back(std::thread([&]()
                                          { st.pop(count); }));
                                          
            sleep(1);//这里需要后sleep,不然可能主线程先执行,子线程还没执行
            std::cout<<count<<std::endl;
            
        }
    }

    for(int i=0;i<threads.size();i++)
    {
        threads[i].join();
    }
    return 0;
}
相关推荐
a努力。4 小时前
腾讯Java面试被问:String、StringBuffer、StringBuilder区别
java·开发语言·后端·面试·职场和发展·架构
长安第一美人4 小时前
php出现zend_mm_heap corrupted 或者Segment fault
开发语言·嵌入式硬件·php·zmq·工业应用开发
Rousson4 小时前
硬件学习笔记--91 TMR型互感器介绍
笔记·学习
Ingsuifon4 小时前
yolov5模型迁移笔记
笔记·yolo
优弧4 小时前
离开舒适区100天,我后悔了吗?
前端·后端·面试
gihigo19984 小时前
基于MATLAB的电力系统经济调度实现
开发语言·matlab
飛6795 小时前
从 0 到 1 掌握 Flutter 状态管理:Provider 实战与原理剖析
开发语言·javascript·ecmascript
龚礼鹏5 小时前
Android应用程序 c/c++ 崩溃排查流程
c语言·开发语言·c++
Filotimo_5 小时前
在java开发中,什么是JSON格式
开发语言·json