并发编程核心原理:可见性、一致性、原子性
一、从一个问题开始
1.1 一个"不可能"的 Bug
cpp
#include <thread>
#include <iostream>
bool ready = false;
int data = 0;
// 线程 1:生产者
void producer() {
data = 42; // 步骤 1: 准备数据
ready = true; // 步骤 2: 标记完成
}
// 线程 2:消费者
void consumer() {
while (!ready); // 等待 ready 变为 true
std::cout << data << std::endl; // 步骤 3: 使用数据
}
int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
return 0;
}
问题:为什么有时会输出 0 而不是 42?
arduino
你以为的执行顺序:
线程 1: data = 42 → ready = true
线程 2: while(!ready) → 读到 true → 读 data = 42 ✓
实际的执行顺序(可能):
线程 1: ready = true → data = 42 ← 被重排序了!
线程 2: while(!ready) → 读到 true → 读 data = 0 ✗
这就是可见性和一致性问题!
二、三大核心概念
2.1 原子性 (Atomicity)
什么是原子性?
原子性 = 操作不可分割
就像原子一样,不能再分
示例:
┌────────────────────────────────────────┐
│ 原子操作: │
│ ┌─────────────────┐ │
│ │ counter++ │ 一气呵成 │
│ └─────────────────┘ │
└────────────────────────────────────────┘
非原子操作(实际是三步):
┌────────────────────────────────────────┐
│ counter++ 实际是: │
│ 1. 读取 counter 的值 │
│ 2. 加 1 │
│ 3. 写回 counter │
│ │
│ 这三步可以被其他线程打断! │
└────────────────────────────────────────┘
原子性问题演示:
cpp
#include <atomic>
#include <thread>
#include <vector>
#include <iostream>
int normal_count = 0; // 非原子
std::atomic<int> atomic_count(0); // 原子
void test_normal() {
for (int i = 0; i < 10000; i++) {
normal_count++; // 非原子:读 - 改 - 写
}
}
void test_atomic() {
for (int i = 0; i < 10000; i++) {
atomic_count++; // 原子:不可分割
}
}
int main() {
std::vector<std::thread> threads;
// 10 个线程同时增加计数器
for (int i = 0; i < 10; i++) {
threads.emplace_back(test_normal);
}
for (auto& t : threads) t.join();
std::cout << "非原子结果:" << normal_count << std::endl;
// 期望 100000,实际可能是 50000-90000 之间的任意值
threads.clear();
normal_count = 0;
for (int i = 0; i < 10; i++) {
threads.emplace_back(test_atomic);
}
for (auto& t : threads) t.join();
std::cout << "原子结果:" << atomic_count << std::endl;
// 一定是 100000!
return 0;
}
2.2 可见性 (Visibility)
css
什么是可见性?
可见性 = 一个线程修改了共享变量,其他线程能立即看到这个修改
问题根源:每个线程有自己的"工作内存"
┌─────────────────────────────────────────────────┐
│ 主内存 (Main Memory) │
│ ┌───────────────┐ │
│ │ shared_var │ │
│ └───────────────┘ │
│ ↑ ↑ │
│ 刷新 │ │ 刷新 │
│ │ │ │
│ ┌─────────────────┴─┐ ┌─────┴─────────────┐ │
│ │ 线程 A 工作内存 │ │ 线程 B 工作内存 │ │
│ │ ┌─────────────┐ │ │ ┌─────────────┐ │ │
│ │ │shared_var=0 │ │ │ │shared_var=0 │ │ │
│ │ └─────────────┘ │ │ └─────────────┘ │ │
│ └───────────────────┘ └───────────────────┘ │
└─────────────────────────────────────────────────┘
线程 A 修改后:
┌─────────────────────────────────────────────────┐
│ 主内存 (Main Memory) │
│ ┌───────────────┐ │
│ │ shared_var │ ← 还是旧值! │
│ └───────────────┘ │
│ ↑ ↑ │
│ 还没刷新 │ │ 还没刷新 │
│ │ │ │
│ ┌─────────────────┴─┐ ┌─────┴─────────────┐ │
│ │ 线程 A 工作内存 │ │ 线程 B 工作内存 │ │
│ │ ┌─────────────┐ │ │ ┌─────────────┐ │ │
│ │ │shared_var=1 │ │ │ │shared_var=0 │ │ │
│ │ └─────────────┘ │ │ └─────────────┘ │ │
│ │ ↑ │ │ │ │
│ │ A 看到了新值 │ │ B 还看到旧值! │ │
│ └───────────────────┘ └───────────────────┘ │
└─────────────────────────────────────────────────┘
可见性问题演示:
cpp
#include <thread>
#include <iostream>
#include <chrono>
bool flag = false; // 普通变量,没有可见性保证
void writer() {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "[Writer] 准备设置 flag = true" << std::endl;
flag = true;
std::cout << "[Writer] 已设置 flag = true" << std::endl;
}
void reader() {
int count = 0;
while (!flag) {
count++;
// 没有 flag 的更新,可能一直循环
// 因为编译器可能优化为:
// if (!flag) { while(true); }
}
std::cout << "[Reader] 检测到 flag 变化,循环次数:" << count << std::endl;
}
int main() {
std::thread t1(writer);
std::thread t2(reader);
t1.join();
t2.join();
return 0;
}
为什么可见性会出问题?
markdown
三个层面的可见性问题:
1. 编译器优化
└─> 缓存变量到寄存器
└─> 认为变量不会被其他线程修改
2. CPU 缓存
└─> 每个 CPU 核心有自己的 L1/L2 缓存
└─> 修改可能只在本地缓存,没写回主内存
3. 指令重排序
└─> CPU 可能重新排列指令顺序
└─> 为了性能优化
2.3 有序性 (Ordering)
ini
什么有序性?
有序性 = 程序执行顺序按照代码的先后顺序
问题:指令重排序
代码顺序: 实际执行顺序:
┌──────────────┐ ┌──────────────┐
│ 1. data = 42 │ │ 2. ready = true │ ← 先执行
│ 2. ready = true│ │ 1. data = 42 │ ← 后执行
└──────────────┘ └──────────────┘
为什么可以重排序?
- 单线程下,结果一样
- CPU 为了性能,会乱序执行
- 编译器为了优化,会调整指令
多线程下,重排序导致问题!
有序性问题演示:
cpp
#include <atomic>
#include <thread>
#include <iostream>
int data = 0;
bool ready = false;
int read_result = -1;
void writer() {
data = 42; // 1. 准备数据
ready = true; // 2. 标记完成
}
void reader() {
while (!ready); // 等待 ready
read_result = data; // 读数据
}
int main() {
for (int i = 0; i < 1000; i++) {
data = 0;
ready = false;
read_result = -1;
std::thread t1(writer);
std::thread t2(reader);
t1.join();
t2.join();
if (read_result == 0) {
std::cout << "第 " << i << " 次:读到旧值 0 (重排序导致)" << std::endl;
}
}
return 0;
}
三、内存模型:解决三大问题
3.1 什么是内存模型?
diff
内存模型 = 规定多线程程序中,内存访问的顺序和可见性规则
就像交通规则:
- 红灯停,绿灯行(可见性规则)
- 靠右行驶(有序性规则)
- 不能同时占用同一车道(原子性规则)
C++11 引入了正式的内存模型!
3.2 六种内存序
cpp
enum memory_order {
memory_order_relaxed, // 宽松序
memory_order_consume, // 消费序(很少用)
memory_order_acquire, // 获取序
memory_order_release, // 释放序
memory_order_acq_rel, // 获取 - 释放序
memory_order_seq_cst // 顺序一致序(默认)
};
3.3 内存序详解
(1) memory_order_relaxed(宽松序)
diff
特点:
- 只保证原子性
- 不保证可见性
- 不保证有序性
- 性能最高
适用场景:计数器
┌────────────────────────────────────────┐
│ 线程 A: counter++ (relaxed) │
│ 线程 B: counter++ (relaxed) │
│ 线程 C: counter++ (relaxed) │
│ │
│ 最终结果正确(原子性保证) │
│ 但各线程看到的中间值可能不一致 │
└────────────────────────────────────────┘
cpp
#include <atomic>
#include <thread>
#include <vector>
#include <iostream>
std::atomic<int> counter(0);
void increment() {
for (int i = 0; i < 1000; i++) {
// 宽松序:只要求原子性
counter.fetch_add(1, std::memory_order_relaxed);
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10; i++) {
threads.emplace_back(increment);
}
for (auto& t : threads) t.join();
std::cout << "最终计数:" << counter << std::endl;
// 一定是 10000(原子性保证)
return 0;
}
(2) memory_order_acquire(获取序)
arduino
特点:
- 用于读操作
- 保证:之后的读写不会被重排到 acquire 之前
- 保证:能看到 release 操作之前的所有写入
类比:获取一把锁
- 获取锁之后,能看到锁保护的所有数据
┌────────────────────────────────────────┐
│ 线程 A: │
│ data = 42; │
│ ready.store(true, release); │
│ │
│ 线程 B: │
│ while (!ready.load(acquire)); │
│ // 这里一定能看到 data = 42 │
└────────────────────────────────────────┘
(3) memory_order_release(释放序)
diff
特点:
- 用于写操作
- 保证:之前的读写不会被重排到 release 之后
- 保证:对 acquire 读可见
类比:释放一把锁
- 释放锁之前,所有修改都对获取锁的线程可见
┌────────────────────────────────────────┐
│ Release 操作就像一个"栅栏" │
│ │
│ 之前的操作 ←───┐ │
│ │ 不能越过栅栏 │
│ 之后的操作 ←───┤ │
│ │ │
│ ═══════════════════════════════════ │
│ Release 栅栏 │
└────────────────────────────────────────┘
(4) memory_order_acq_rel(获取 - 释放序)
diff
特点:
- acquire + release
- 用于读 - 写操作(如 exchange、fetch_add)
┌────────────────────────────────────────┐
│ 获取序 │ 释放序 │
│ ↓ ↓ │
│ ═══════════════════════════════════ │
│ Acq_Rel 栅栏 │
└────────────────────────────────────────┘
(5) memory_order_seq_cst(顺序一致序)
diff
特点:
- 最严格的内存序
- 所有线程看到相同的操作顺序
- 默认选项
- 性能最低
┌────────────────────────────────────────┐
│ 所有线程看到的全局顺序: │
│ │
│ 线程 A: 操作 1 → 操作 2 → 操作 3 │
│ 线程 B: 操作 1 → 操作 2 → 操作 3 │
│ 线程 C: 操作 1 → 操作 2 → 操作 3 │
│ │
│ 就像有一个全局时钟,所有操作按顺序执行 │
└────────────────────────────────────────┘
3.4 内存序对比表
| 内存序 | 原子性 | 可见性 | 有序性 | 性能 | 适用场景 |
|---|---|---|---|---|---|
| relaxed | ✓ | ✗ | ✗ | 最高 | 计数器 |
| consume | ✓ | 部分 | 部分 | 高 | 依赖数据(很少用) |
| acquire | ✓ | ✓ | 部分 | 中高 | 获取锁/标志 |
| release | ✓ | ✓ | 部分 | 中高 | 释放锁/标志 |
| acq_rel | ✓ | ✓ | ✓ | 中 | 原子交换 |
| seq_cst | ✓ | ✓ | ✓ | 低 | 默认,需要强一致 |
四、实际应用:从理论到代码
4.1 场景 1:自旋锁实现
cpp
#include <atomic>
#include <thread>
#include <iostream>
class SpinLock {
std::atomic<bool> locked{false};
public:
void lock() {
// 自旋等待,直到获取锁
while (locked.exchange(true, std::memory_order_acquire)) {
// 忙等待
std::this_thread::yield(); // 让出 CPU
}
}
void unlock() {
// 释放锁
locked.store(false, std::memory_order_release);
}
};
// 为什么用 acquire-release?
/*
lock():
- exchange(true, acquire): 获取锁,看到之前持有锁的线程的所有修改
- 保证进入临界区后,能看到共享数据的最新值
unlock():
- store(false, release): 释放锁,之前的修改对下一个获取锁的线程可见
- 保证临界区内的修改对其他线程可见
*/
int shared_data = 0;
SpinLock spinlock;
void worker(int id) {
for (int i = 0; i < 1000; i++) {
spinlock.lock();
// 临界区
int temp = shared_data;
std::this_thread::yield(); // 模拟工作
shared_data = temp + 1;
spinlock.unlock();
}
}
int main() {
std::thread t1(worker, 1);
std::thread t2(worker, 2);
t1.join();
t2.join();
std::cout << "最终值:" << shared_data << std::endl;
// 一定是 2000
return 0;
}
4.2 场景 2:无锁队列(简化版)
cpp
#include <atomic>
#include <memory>
template<typename T>
class LockFreeQueue {
struct Node {
T data;
std::atomic<Node*> next{nullptr};
Node() = default;
Node(T val) : data(val) {}
};
std::atomic<Node*> head;
std::atomic<Node*> tail;
public:
LockFreeQueue() {
Node* dummy = new Node();
head.store(dummy, std::memory_order_relaxed);
tail.store(dummy, std::memory_order_relaxed);
}
~LockFreeQueue() {
while (Node* node = head.load(std::memory_order_relaxed)) {
head.store(node->next.load(std::memory_order_relaxed),
std::memory_order_relaxed);
delete node;
}
}
void enqueue(T value) {
Node* newNode = new Node(value);
while (true) {
Node* last = tail.load(std::memory_order_acquire);
Node* next = last->next.load(std::memory_order_acquire);
// 检查 tail 是否还是最新的
if (last == tail.load(std::memory_order_acquire)) {
if (next == nullptr) {
// 尝试将新节点链接到末尾
if (last->next.compare_exchange_weak(next, newNode,
std::memory_order_release,
std::memory_order_relaxed)) {
// 链接成功,更新 tail
tail.compare_exchange_strong(last, newNode,
std::memory_order_release,
std::memory_order_relaxed);
return;
}
} else {
// tail 落后了,帮助推进
tail.compare_exchange_weak(last, next,
std::memory_order_release,
std::memory_order_relaxed);
}
}
}
}
bool dequeue(T& result) {
while (true) {
Node* first = head.load(std::memory_order_acquire);
Node* last = tail.load(std::memory_order_acquire);
Node* next = first->next.load(std::memory_order_acquire);
if (first == head.load(std::memory_order_acquire)) {
if (first == last) {
if (next == nullptr) {
return false; // 队列空
}
// tail 落后了,帮助推进
tail.compare_exchange_weak(last, next,
std::memory_order_release,
std::memory_order_relaxed);
} else {
// 读取值
result = next->data;
if (head.compare_exchange_weak(first, next,
std::memory_order_release,
std::memory_order_relaxed)) {
delete first;
return true;
}
}
}
}
}
};
4.3 场景 3:单例模式(双重检查锁定)
cpp
#include <atomic>
#include <mutex>
class Singleton {
private:
static std::atomic<Singleton*> instance;
static std::mutex mtx;
Singleton() {}
public:
static Singleton* getInstance() {
// 第一次检查(不需要锁)
Singleton* tmp = instance.load(std::memory_order_acquire);
if (tmp != nullptr) {
return tmp;
}
// 需要创建实例,加锁
std::lock_guard<std::mutex> lock(mtx);
// 第二次检查(在锁内)
tmp = instance.load(std::memory_order_relaxed);
if (tmp == nullptr) {
tmp = new Singleton();
instance.store(tmp, std::memory_order_release);
}
return tmp;
}
};
std::atomic<Singleton*> Singleton::instance{nullptr};
std::mutex Singleton::mtx;
/*
为什么这样写?
1. 第一次检查用 acquire:
- 确保看到 instance 不为 null 时
- 也能看到 Singleton 构造函数的所有修改
2. store 用 release:
- 确保构造函数完成后再设置 instance
- 其他线程看到 instance 不为 null 时,对象已完全构造
3. 不用 seq_cst:
- acquire-release 足够保证正确性
- 性能更好
*/
4.4 场景 4:读写锁
cpp
#include <atomic>
#include <thread>
#include <shared_mutex>
class ReadWriteLock {
std::atomic<int> read_count{0};
std::atomic<bool> writing{false};
public:
void lock_read() {
// 增加读计数
int expected = read_count.load(std::memory_order_relaxed);
while (!read_count.compare_exchange_weak(expected, expected + 1,
std::memory_order_acquire,
std::memory_order_relaxed)) {
// 重试
}
// 等待写操作完成
while (writing.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
}
void unlock_read() {
read_count.fetch_sub(1, std::memory_order_release);
}
void lock_write() {
// 等待所有读完成
bool expected = false;
while (!writing.compare_exchange_weak(expected, true,
std::memory_order_acquire,
std::memory_order_relaxed)) {
expected = false;
std::this_thread::yield();
}
// 等待所有读者离开
while (read_count.load(std::memory_order_acquire) > 0) {
std::this_thread::yield();
}
}
void unlock_write() {
writing.store(false, std::memory_order_release);
}
};
五、深入理解:硬件层面
5.1 CPU 缓存架构
diff
现代 CPU 缓存层次:
┌─────────────────────────────────────────────────┐
│ CPU Core 0 CPU Core 1 │
│ ┌─────────┐ ┌─────────┐ │
│ │ L1 缓存 │ │ L1 缓存 │ │
│ │ 32KB │ │ 32KB │ │
│ └────┬────┘ └────┬────┘ │
│ │ │ │
│ ┌────┴────────────┴────┐ │
│ │ L2 缓存 │ │
│ │ 256KB │ │
│ └──────────┬───────────┘ │
│ │ │
│ ┌──────────┴───────────┐ │
│ │ L3 缓存 │ │
│ │ 共享缓存 │ │
│ └──────────┬───────────┘ │
│ │ │
│ ┌──────────┴───────────┐ │
│ │ 主内存 │ │
│ │ (DRAM) │ │
│ └──────────────────────┘ │
└─────────────────────────────────────────────────┘
可见性问题根源:
- 每个核心有自己的 L1 缓存
- 修改可能只在本地 L1,其他核心看不到
- 需要缓存一致性协议(MESI)来同步
5.2 MESI 协议
ini
MESI = Modified + Exclusive + Shared + Invalid
四种缓存行状态:
┌─────────────────────────────────────────────────┐
│ M (Modified): 修改态 │
│ - 缓存行被修改,与内存不一致 │
│ - 只有这一个缓存有该数据 │
│ - 必须写回内存 │
├─────────────────────────────────────────────────┤
│ E (Exclusive): 独占态 │
│ - 缓存行与内存一致 │
│ - 只有这一个缓存有该数据 │
│ - 可以直接修改(变成 M) │
├─────────────────────────────────────────────────┤
│ S (Shared): 共享态 │
│ - 缓存行与内存一致 │
│ - 多个缓存可能有该数据 │
│ - 修改前需要通知其他缓存失效 │
├─────────────────────────────────────────────────┤
│ I (Invalid): 无效态 │
│ - 缓存行无效 │
│ - 需要重新从内存或其他缓存加载 │
└─────────────────────────────────────────────────┘
状态转换:
读 写
I ─────────> E ─────────> M
↑ ↓
└──── S <───┘
5.3 内存屏障 (Memory Barrier)
css
什么是内存屏障?
内存屏障 = 指令,告诉 CPU 不要重排屏障两侧的指令
就像一堵墙:
┌────────────────────────────────────────┐
│ 指令 A │
│ 指令 B │
│ ↓ │
│ ════════════════════ │
│ 内存屏障 │
│ ════════════════════ │
│ ↑ │
│ 指令 C │
│ 指令 D │
│ │
│ A 和 B 可以在屏障前重排 │
│ C 和 D 可以在屏障后重排 │
│ 但 A/B 不能和 C/D 跨越屏障重排 │
└────────────────────────────────────────┘
C++ 中的内存屏障:
std::atomic_thread_fence(std::memory_order_acquire);
std::atomic_thread_fence(std::memory_order_release);
std::atomic_thread_fence(std::memory_order_seq_cst);
5.4 虚假共享 (False Sharing)
c
什么是虚假共享?
多个线程修改同一缓存行的不同变量,导致缓存行频繁失效
┌─────────────────────────────────────────────────┐
│ 缓存行 (64 字节) │
│ ┌──────┬──────┬──────┬──────┬──────┬──────┐ │
│ │ varA │ varB │ varC │ varD │ ... │ ... │ │
│ └──────┴──────┴──────┴──────┴──────┴──────┘ │
│ ↑ ↑ │
│ 线程 1 修改 线程 2 修改 │
│ │
│ 虽然修改的是不同变量,但在同一缓存行! │
│ 导致缓存行在两个核心间来回同步 │
│ 性能严重下降! │
└─────────────────────────────────────────────────┘
解决方案:内存对齐
struct alignas(64) PaddedCounter {
std::atomic<int> counter1;
char padding1[60]; // 填充到 64 字节
std::atomic<int> counter2;
char padding2[60]; // 填充到 64 字节
};
六、最佳实践总结
6.1 选择内存序的决策树
arduino
┌─────────────────┐
│ 需要原子操作吗? │
└────────┬────────┘
│
┌──────────────┼──────────────┐
│ 否 │ 是 │
│ ▼ │
│ ┌─────────────┐ │
│ │ 需要可见性吗?│ │
│ └──────┬──────┘ │
│ │ │
│ ┌─────────┼─────────┐ │
│ │ 否 │ 是 │ │
│ ▼ ▼ │ │
│ relaxed 需要强顺序? │ │
│ │ │ │
│ ┌─────────┼─────────┐│ │
│ │ 否 │ 是 ││ │
│ ▼ ▼ ││ │
│ acquire- seq_cst ││ │
│ release ││ │
└───────────────────────┘│ │
▼ ▼
用 mutex 或 seq_cst
6.2 实用建议
cpp
// 1. 默认使用 seq_cst(最简单,不容易错)
std::atomic<int> counter(0); // 默认 seq_cst
counter++; // 安全
// 2. 计数器用 relaxed(性能最好)
std::atomic<int> stats(0);
stats.fetch_add(1, std::memory_order_relaxed);
// 3. 标志位用 acquire-release
std::atomic<bool> ready(false);
// 写
ready.store(true, std::memory_order_release);
// 读
if (ready.load(std::memory_order_acquire)) {
// 能看到写之前的所有数据
}
// 4. 复杂场景用 mutex
std::mutex mtx;
std::lock_guard<std::mutex> lock(mtx);
// 简单、安全、不容易错
// 5. 避免在原子变量上依赖顺序
std::atomic<int> x(0), y(0);
// 不推荐:依赖 x 和 y 的顺序
x.store(1, std::memory_order_release);
y.store(1, std::memory_order_release);
// 推荐:用 seq_cst 或 mutex
6.3 调试技巧
cpp
// 1. 使用 ThreadSanitizer 检测数据竞争
// 编译时加 -fsanitize=thread
g++ -fsanitize=thread -g program.cpp -o program
// 2. 使用原子变量的 debug 模式
#ifdef DEBUG
#define ATOMIC_OP(op, var, ...) do { \
std::cout << "Thread " << std::this_thread::get_id() \
<< ": " << #var << "." << #op << std::endl; \
var.op(__VA_ARGS__); \
} while(0)
#else
#define ATOMIC_OP(op, var, ...) var.op(__VA_ARGS__)
#endif
// 3. 添加日志追踪
class TrackedAtomic {
std::atomic<int> value;
std::string name;
public:
TrackedAtomic(const std::string& n) : name(n) {}
void store(int v) {
std::cout << "[" << name << "] store " << v
<< " (thread " << std::this_thread::get_id() << ")" << std::endl;
value.store(v);
}
int load() {
int v = value.load();
std::cout << "[" << name << "] load " << v
<< " (thread " << std::this_thread::get_id() << ")" << std::endl;
return v;
}
};
七、总结
7.1 核心概念回顾
| 概念 | 问题 | 解决方案 |
|---|---|---|
| 原子性 | 操作被中断 | atomic 操作 |
| 可见性 | 修改看不到 | acquire-release / 锁 |
| 有序性 | 指令重排序 | 内存屏障 / seq_cst |
7.2 一句话总结
原子性:操作要么全做,要么全不做
可见性:我的修改你能看到
有序性:代码顺序就是执行顺序
内存模型:规定这三者的规则
7.3 实际开发建议
arduino
1. 优先使用高级抽象
mutex > 原子操作 > 手动内存序
2. 默认用 seq_cst
除非性能瓶颈,再考虑 relaxed/acquire-release
3. 减少共享
线程本地存储 > 共享变量
4. 使用成熟库
不要自己实现无锁数据结构
5. 用工具检测
ThreadSanitizer、helgrind 等