C+并发编程指南15 原子操作和原子类型

发布于:2025-09-06 ⋅ 阅读:(14) ⋅ 点赞:(0)

5.2 原子操作和原子类型

在多线程程序里,有个非常重要的概念叫 原子操作
所谓“原子”,意思就是:
👉 这个操作要么完整执行,要么完全没发生,绝不会出现“执行了一半”的情况。

打个比方:

  • 原子操作 就像你开关灯,要么灯是开,要么灯是关,中间不会有人看到“半亮半暗”的状态。
  • 非原子操作 就可能像换灯泡,你手刚拧下灯泡一半,别人看见灯既没完全亮,也没完全灭,结果导致“奇怪的中间状态”。

如果线程之间访问同一个变量时不是原子操作,就可能出现 数据竞争,从而导致未定义行为。


5.2.1 标准原子类型

C++ 提供了 <atomic> 头文件,里面有一系列 原子类型
比如:

  • std::atomic<int> → 原子整型
  • std::atomic<bool> → 原子布尔
  • std::atomic<T*> → 原子指针
  • std::atomic_flag → 最简单的原子布尔标志

这些类型内部会用硬件的原子指令(如果平台支持),否则退而求其次用锁模拟。你可以通过 is_lock_free() 来检查某个原子类型是不是无锁实现。

⚡ 注意:只有 std::atomic_flag 是保证无锁的。


5.2.2 std::atomic_flag —— 最简单的原子类型

这是最简单的原子类型,只能做三件事:

  1. 初始化(必须用 ATOMIC_FLAG_INIT
  2. test_and_set():设置为 true,并返回旧值
  3. clear():清除为 false

它经常用来实现一个 自旋锁 (spinlock)

下面我们写个完整的例子:多个线程一起往一个 std::vector<int> 里添加数据,但用 spinlock_mutex 来保护。

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

class spinlock_mutex {
    std::atomic_flag flag;
public:
    spinlock_mutex() : flag(ATOMIC_FLAG_INIT) {}

    void lock() {
        while (flag.test_and_set(std::memory_order_acquire)); // 一直忙等,直到获取锁
    }
    void unlock() {
        flag.clear(std::memory_order_release);
    }
};

spinlock_mutex my_lock;
std::vector<int> shared_data;

void worker(int id) {
    for (int i = 0; i < 5; i++) {
        my_lock.lock();
        shared_data.push_back(id * 10 + i);
        my_lock.unlock();
    }
}

int main() {
    std::thread t1(worker, 1);
    std::thread t2(worker, 2);

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

    for (int v : shared_data) {
        std::cout << v << " ";
    }
    std::cout << std::endl;
}

运行后你会看到两个线程都安全地往 shared_data 里写入数据。


5.2.3 std::atomic<bool>

std::atomic<bool> 是更通用的布尔原子类型,它支持更多操作,比如:

  • store() 写入
  • load() 读取
  • exchange() 交换
  • compare_exchange_weak() / compare_exchange_strong() 比较并交换(CAS)

示例:
两个线程竞争某个布尔开关,谁先把它从 false 改成 true,谁就“赢了”。

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

std::atomic<bool> ready(false);

void try_to_win(int id) {
    bool expected = false;
    if (ready.compare_exchange_strong(expected, true)) {
        std::cout << "Thread " << id << " got the flag!\n";
    } else {
        std::cout << "Thread " << id << " lost...\n";
    }
}

int main() {
    std::thread t1(try_to_win, 1);
    std::thread t2(try_to_win, 2);

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

每次运行,只有一个线程会赢,另一个会失败。


5.2.4 std::atomic<T*> —— 原子指针

它支持指针运算,比如 fetch_add() 可以让指针往后移动。

例子:多个线程安全地遍历数组。

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

struct Foo {
    int value;
};

Foo arr[5] = { {1}, {2}, {3}, {4}, {5} };
std::atomic<Foo*> ptr(arr);

void worker(int id) {
    Foo* old = ptr.fetch_add(1); // 原子地取出旧值并让指针+1
    if (old < arr + 5) {
        std::cout << "Thread " << id << " got value " << old->value << "\n";
    }
}

int main() {
    std::thread threads[5];
    for (int i = 0; i < 5; i++) {
        threads[i] = std::thread(worker, i+1);
    }
    for (auto& t : threads) t.join();
}

结果:每个线程取到数组的一个元素,互不冲突。


5.2.5 原子整型

常用的操作:

  • fetch_add()fetch_sub()
  • fetch_and()fetch_or()fetch_xor()
  • 自增 ++ / 自减 --

我们写个线程安全的计数器:

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

std::atomic<int> counter(0);

void increment(int id) {
    for (int i = 0; i < 1000; i++) {
        counter++;
    }
}

int main() {
    std::thread t1(increment, 1);
    std::thread t2(increment, 2);

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

    std::cout << "Final counter = " << counter.load() << std::endl;
}

最后输出 2000,说明没有丢计数。


5.2.6 通用 std::atomic<T>

如果你想要对自定义类型使用原子操作,可以用 std::atomic<T>,但限制比较多:

  • 不能有虚函数、虚继承
  • 必须能用 memcpy 拷贝
  • 比较时是按字节比较(memcmp),不是用户定义的 operator==

例子:定义一个简单的 struct,然后做原子交换。

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

struct Point {
    int x, y;
};

std::atomic<Point> ap;

void worker(int id) {
    Point p{id, id*2};
    Point old = ap.exchange(p); // 原子替换
    std::cout << "Thread " << id << " set (" << p.x << "," << p.y 
              << "), old was (" << old.x << "," << old.y << ")\n";
}

int main() {
    ap.store({0,0});

    std::thread t1(worker, 1);
    std::thread t2(worker, 2);

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

你会看到两个线程交替写入和读取 Point


5.2.7 非成员函数形式

C++ 还提供了和成员函数等价的 非成员函数,主要是为了兼容 C。比如:

#include <atomic>
#include <iostream>

int main() {
    std::atomic<int> a(10);

    int x = std::atomic_load(&a);
    std::cout << "Load = " << x << std::endl;

    std::atomic_store(&a, 20);
    std::cout << "After store = " << a.load() << std::endl;
}

总结

  • std::atomic_flag:最简单的无锁布尔,用来实现自旋锁。
  • std::atomic<bool>:更通用的布尔原子,支持 CAS。
  • std::atomic<T*>:指针的原子操作,支持加减。
  • 原子整型:支持加减、位运算、自增自减,常用于计数器。
  • 通用 std::atomic<T>:能对简单的用户定义类型做原子操作。
  • 非成员函数:提供 C 兼容的接口。


好嘞 👍 我帮你把这份代码逐行加上 中文注释,而且会解释 为什么运行会得到这样的结果。这样你在学习的时候可以对照着源码 + 注释理解。


带详细中文注释的版本

#include <atomic>      // 原子操作相关的头文件
#include <iostream>    // 标准输入输出
#include <thread>      // 多线程支持
#include <vector>      // 动态数组容器

// ======================================================
// 例子 1: atomic_flag 实现自旋锁
// ======================================================

// 用 atomic_flag 实现一个简单的自旋锁
class spinlock_mutex {
    std::atomic_flag flag;  // 原子标志,用来表示锁是否被占用
public:
    // 构造函数:用 clear() 把标志位初始化为 false (未加锁)
    spinlock_mutex() { flag.clear(); }

    // 加锁操作
    void lock() {
        // test_and_set() 会把 flag 设为 true,并返回旧值
        // 如果旧值是 true,说明锁被占用,就继续忙等
        while (flag.test_and_set(std::memory_order_acquire));
    }

    // 解锁操作
    void unlock() {
        // clear() 把 flag 设为 false,表示释放锁
        flag.clear(std::memory_order_release);
    }
};

spinlock_mutex my_lock;        // 定义一个自旋锁
std::vector<int> shared_data;  // 共享数据,多个线程要往里面写入

// 工作者线程:往 shared_data 里写 5 个数
void worker_flag(int id) {
    for (int i = 0; i < 5; i++) {
        my_lock.lock();                    // 加锁,保证只有一个线程能进入
        shared_data.push_back(id * 10 + i);// 写数据
        my_lock.unlock();                  // 解锁
    }
}

// 演示 atomic_flag
void demo_atomic_flag() {
    shared_data.clear();  // 清空数据
    std::thread t1(worker_flag, 1);
    std::thread t2(worker_flag, 2);
    t1.join();
    t2.join();

    std::cout << "[atomic_flag] shared_data: ";
    for (int v : shared_data) std::cout << v << " ";
    std::cout << "\n";
    // 结果:两个线程安全地写入数据,不会崩溃或数据错乱
    // 但输出顺序可能不同,比如 "10 11 12 13 14 20 21 22 23 24"
}

// ======================================================
// 例子 2: atomic<bool> CAS 操作
// ======================================================

std::atomic<bool> ready(false); // 一个原子布尔值,初始为 false

// 尝试争夺布尔开关的线程函数
void try_to_win(int id) {
    bool expected = false; // 期望值
    // compare_exchange_strong():如果 ready == expected,就把它设为 true
    if (ready.compare_exchange_strong(expected, true)) {
        std::cout << "Thread " << id << " got the flag!\n"; // 成功的线程
    }
    else {
        std::cout << "Thread " << id << " lost...\n";       // 失败的线程
    }
}

void demo_atomic_bool() {
    ready.store(false); // 每次测试前重置为 false
    std::thread t1(try_to_win, 1);
    std::thread t2(try_to_win, 2);
    t1.join();
    t2.join();
    // 结果:只有一个线程能成功把 false 改为 true,另一个会失败
    // 每次运行可能 t1 赢,也可能 t2 赢
}

// ======================================================
// 例子 3: atomic<T*> 指针操作
// ======================================================

struct Foo {
    int value;
};

Foo arr[5] = { {1}, {2}, {3}, {4}, {5} }; // 一个数组
std::atomic<Foo*> ptr(arr);                // 原子指针,初始指向 arr[0]

// 工作者线程:原子地获取并移动指针
void worker_ptr(int id) {
    Foo* old = ptr.fetch_add(1); // 返回旧指针,并让指针+1
    if (old < arr + 5) {         // 确保没越界
        std::cout << "Thread " << id << " got value " << old->value << "\n";
    }
}

void demo_atomic_pointer() {
    ptr.store(arr); // 重置指针到数组开头
    std::thread threads[5];
    for (int i = 0; i < 5; i++) {
        threads[i] = std::thread(worker_ptr, i + 1);
    }
    for (auto& t : threads) t.join();
    // 结果:5 个线程各自拿到 arr 里的不同元素,互不冲突
    // 输出顺序不固定,但每个值 1~5 都会被打印一次
}

// ======================================================
// 例子 4: 原子整型计数器
// ======================================================

std::atomic<int> counter(0); // 原子整型计数器,初始为 0

// 每个线程循环自增 1000 次
void increment(int id) {
    for (int i = 0; i < 1000; i++) {
        counter++; // 原子操作,保证不会丢计数
    }
}

void demo_atomic_int() {
    counter.store(0); // 重置为 0
    std::thread t1(increment, 1);
    std::thread t2(increment, 2);
    t1.join();
    t2.join();

    std::cout << "[atomic<int>] Final counter = " << counter.load() << "\n";
    // 结果:最终 counter = 2000,证明两个线程的自增操作是安全的
}

// ======================================================
// 例子 5: 通用 atomic<T> 用户自定义类型
// ======================================================

struct Point {
    int x, y;
};

std::atomic<Point> ap; // 原子存储一个结构体 Point

// 工作者线程:用 exchange() 替换值
void worker_point(int id) {
    Point p{ id, id * 2 };       // 新值
    Point old = ap.exchange(p);  // 原子替换,返回旧值
    std::cout << "Thread " << id << " set (" << p.x << "," << p.y
              << "), old was (" << old.x << "," << old.y << ")\n";
}

void demo_atomic_struct() {
    ap.store({ 0,0 }); // 初始值 (0,0)
    std::thread t1(worker_point, 1);
    std::thread t2(worker_point, 2);
    t1.join();
    t2.join();
    // 结果:两个线程依次替换结构体
    // 可能输出:
    // Thread 1 set (1,2), old was (0,0)
    // Thread 2 set (2,4), old was (1,2)
}

// ======================================================
// 例子 6: 非成员函数接口
// ======================================================

void demo_nonmember_functions() {
    std::atomic<int> a(10); // 原子整型,初始值 10

    int x = std::atomic_load(&a); // 非成员函数 load
    std::cout << "[nonmember] Load = " << x << "\n"; // 输出 10

    std::atomic_store(&a, 20); // 非成员函数 store
    std::cout << "[nonmember] After store = " << a.load() << "\n"; // 输出 20
}

// ======================================================
// 主函数:依次运行所有 demo
// ======================================================

int main() {
    std::cout << "===== Atomic Examples =====\n";

    demo_atomic_flag();        // 演示自旋锁
    demo_atomic_bool();        // 演示 CAS
    demo_atomic_pointer();     // 演示原子指针
    demo_atomic_int();         // 演示原子计数器
    demo_atomic_struct();      // 演示原子结构体
    demo_nonmember_functions();// 演示非成员函数接口

    return 0;
}

总结(为什么结果是这样)

  1. atomic_flag → 自旋锁保护共享数据,保证两个线程写入 vector 不会乱。
  2. atomic → 只有一个线程能把 false → true,另一个失败。
  3. *atomic<T>** → 每个线程安全地拿到数组里不同的元素。
  4. atomic → 自增 2000 次不会丢计数。
  5. atomic → 两个线程依次替换结构体,能看到前一个值。
  6. 非成员函数 → load/store 和成员函数效果一样,只是另一种写法。

要不要我再帮你画一张 流程图 (mermaid),把这几个例子之间的知识点关系梳理成一个知识地图?

好嘞,我给你画一张 原子操作示例知识关系图,用 Mermaid 语法表示,这样能直观看到各个例子之间的关系和应用场景。

Atomic Examples 总览
atomic_flag 自旋锁
atomic CAS 操作
atomic 原子指针操作
atomic 原子计数器
atomic 原子结构体
非成员函数接口: atomic_load / atomic_store
保护共享数据 vector, 避免数据冲突
竞争布尔开关, 只有一个线程能成功
原子获取数组元素, 多线程安全访问
线程安全计数, 自增不丢计数
原子替换用户自定义类型, 可返回旧值
提供非成员函数接口, 功能等价于成员函数

说明:

  • B 自旋锁:用 atomic_flag 来保护共享资源,保证多线程安全。
  • C CAS 操作:用 compare_exchange_strong 做一次性竞争。
  • D 原子指针:多线程安全地移动指针,避免数组越界冲突。
  • E 原子计数器:线程安全自增,保证累加结果正确。
  • F 原子结构体:可以直接对结构体类型进行原子操作。
  • G 非成员函数接口:提供了 atomic_loadatomic_store 的替代写法,功能和成员函数一致。

如果你愿意,我可以帮你把这张 Mermaid 图 升级为带示例运行结果的可视化流程图,这样每个节点不仅知道功能,还能直接看到输出示例,更直观学习。

你希望我做吗?


网站公告

今日签到

点亮在社区的每一天
去签到