内存序与无锁编程
C++内存序与无锁编程详解:std::memory_order、std::atomic、fence、ABA 问题与无锁数据结构。
前置知识
- C++ 模板:建议先完成前一篇的学习
学习目标
- 掌握「1. 历史动机与发展脉络」的核心机制、典型用法与常见陷阱
- 掌握「2. 形式化定义」的核心机制、典型用法与常见陷阱
- 掌握「3. 理论推导与原理解析」的核心机制、典型用法与常见陷阱
- 掌握「4. 代码示例(企业级 production-ready)」的核心机制、典型用法与常见陷阱
- 掌握「5. 对比分析」的核心机制、典型用法与常见陷阱
1. 历史动机与发展脉络
1.1 多核时代的内存模型挑战
单核时代,编译器与 CPU 通过顺序一致性(sequential consistency, SC)抽象隐藏了内存访问的复杂性。但多核系统中:
- CPU 缓存层级:L1/L2/L3 多级缓存导致核心间数据视图不一致;
- 写缓冲(Store Buffer):x86 引入写缓冲导致 store-load 重排;
- 乱序执行(Out-of-Order Execution):CPU 动态调度指令以提升 IPC;
- 编译器优化:编译器在单线程视角下可自由重排指令。
例如,以下代码在弱内存模型架构上可能输出 y=0, z=0(Peterson 算法失效):
// 线程 1 // 线程 2
x = 1; y = 1;
r1 = y; r2 = x;
// 期望 r1=1 或 r2=1,但 ARM/POWER 上可能 r1=0, r2=0
1.2 C++11 内存模型的引入
C++11 标准首次为 C++ 定义了形式化的内存模型(memory model),见 ISO/IEC 14882:2011 §1.10。关键提案:
- N2007 Memory Model for C++(Hans-J. Boehm, 2005):奠基性提案,定义 happens-before 关系。
- N2052 A Primer on Synchronization and Sequential Consistency(Boehm, 2006):澄清 SC 与数据竞争。
- N2145 Atomic Operations in C++(Lawrence Crowl, 2007):原子操作 API 设计。
- N2348 A Strong and Safe Memory Model for C++(Boehm, 2007):最终版本。
- N2427 Atomics in the C++ Standard(Crowl, 2007):API 细节。
- N2752 Concurrency in C++(Lawrence Crowl, 2008):完整并发章节。
C++11 模型基于 Java Memory Model(JSR-133)改良,但去掉了 Java 的 volatile 语义与 final field 规则,更接近硬件内存模型。
1.3 C++14/17/20/23/26 演进
| 标准 | 关键变化 | 文档编号 |
|---|---|---|
| C++11 | 引入 std::atomic、6 种 std::memory_order、std::atomic_thread_fence、std::atomic_signal_fence、std::mutex 系列 | N2348, N2752 |
| C++14 | std::shared_timed_mutex 用于读写锁;constexpr 原子操作 | N3659 |
| C++17 | std::scoped_lock 多锁获取;memory_order_consume 规范进入修订、临时不建议使用 | P0156 |
| C++20 | std::atomic_ref<T>(对已有对象的原子访问,P0019);std::counting_semaphore、std::latch、std::barrier;std::atomic 扩展到 std::shared_ptr;atomic::wait/notify | P0019, P0718, P1135 |
| C++23 | std::atomic 增补 fetch_min/fetch_max(含浮点类型) | P0493 |
| C++26(草案) | Hazard pointer(P2530)、std::rcu(P2545);std::execution sender/receiver 框架(P2300);memory_order_consume 正式废弃 | P2530, P2545, P2300 |
1.4 与其他语言的横向对比
| 特性 | C++ | Java | Rust | Go | C# |
|---|---|---|---|---|---|
| 内存模型形式化 | C++11 | JMM (JSR-133) | 借鉴 C++ 与 JMM | Go MM | ECMA-335 |
| 原子操作 API | std::atomic<T> | VarHandle/AtomicXxx | std::sync::atomic | sync/atomic | Interlocked/Volatile |
| 内存序选项 | 6 种 | volatile、final、happens-before | 5 种(无 consume) | LoadSeqCst 等 3 种 | 4 种 |
| 无锁数据结构库 | 第三方(boost::lockfree) | java.util.concurrent | crossbeam | sync/atomic + 自实现 | System.Collections.Concurrent |
| cache line padding | alignas(64) | @Contended | #[repr(align(64))] | //go:noescape + 自定义 | [StructLayout] |
2. 形式化定义
2.1 C++ 内存模型基础
C++ 内存模型将程序视为对抽象机器上内存位置的访问序列。形式化定义如下(ISO/IEC 14882:2024 §6.9.2):
- 内存位置(memory location):标量对象(如
int、char)或相邻位域组成的最大序列。 - 求值(evaluation):包括 value computation(值计算)和 side effect(副作用,如写入内存)。
- 数据竞争(data race):两个线程对同一内存位置进行至少一个写操作,且无 happens-before 关系时,构成数据竞争,导致 UB。
2.2 Happens-before 关系
形式化定义 happens-before 关系 :
- sequenced-before:单线程内,依据语言规则确定的求值顺序。
- synchronizes-with:跨线程同步关系,通常由原子操作或 fence 建立。
2.3 6 种内存序的形式化语义
std::memory_order 枚举定义于 <atomic>,6 个值的语义如下:
| 枚举值 | 语义 | 同步保证 |
|---|---|---|
memory_order_relaxed | 仅保证原子性,不提供顺序保证 | 无 |
memory_order_consume | 名义上仅约束「数据依赖」该 load 的操作;自 C++17 起规范进入修订、不建议使用,主流编译器一律按 acquire 实现,C++26 起正式废弃 | 事实等价于 acquire |
memory_order_acquire | 当前线程内后续的读/写不能重排到 load 之前 | 与 release 配对 |
memory_order_release | 当前线程内之前的读/写不能重排到 store 之后 | 与 acquire 配对 |
memory_order_acq_rel | load 与 store 都有相应保证 | acquire + release |
memory_order_seq_cst | 全局总序,所有线程观察到的顺序一致 | 最强 |
形式化表达:
- relaxed: 仅保证原子性,不建立 synchronizes-with 关系。
- acquire/release:若 为
releasestore 且 为acquireload 且读取了 写入的值,则 ,进一步推导出 。 - seq_cst:在 acquire/release 基础上,附加存在单一全局总序 ,所有 seq_cst 操作按 顺序被所有线程观察到。
2.4 synchronizes-with 关系建立
synchronizes-with 关系通过以下机制建立:
-
Release-Acquire 配对:
- 线程 A 执行
x.store(v, release); - 线程 B 执行
x.load(acquire)且读取到值 ; - 则 A 的 release 之前的所有写操作对 B 的 acquire 之后的所有读/写操作可见。
- 线程 A 执行
-
Release-Consume 配对(C++17 起 deprecated,C++23 重新规范):
- 仅同步依赖该 load 的操作。
-
Fence-Fence 配对:
- A 侧
atomic_thread_fence(release)+ B 侧atomic_thread_fence(acquire)+ 共享原子变量。
- A 侧
-
Seq_cst 全序:
- 所有 seq_cst 操作构成单一全局序 ,所有线程观察一致。
2.5 内存屏障的形式化
std::atomic_thread_fence(order) 引入的屏障对指令重排的影响:
order | 屏障语义 |
|---|---|
relaxed | 无屏障作用 |
acquire | 屏障后的读/写不能重排到屏障前 |
release | 屏障前的读/写不能重排到屏障后 |
acq_rel | acquire + release |
seq_cst | acquire + release + 全局总序 |
2.6 形式化示例:单生产者-单消费者
std::atomic<bool> ready{false};
int data = 0;
// 线程 A(生产者)
void producer() {
data = 42; // (1) 普通写
ready.store(true, std::memory_order_release); // (2) release store
}
// 线程 B(消费者)
void consumer() {
while (!ready.load(std::memory_order_acquire)) {} // (3) acquire load
assert(data == 42); // (4) 读取,保证看到 42
}
形式化推导:
(1)sequenced-before(2)(同线程顺序);(2)synchronizes-with(3)(release-acquire 配对,B 读到 true);(3)sequenced-before(4)(同线程顺序);- 由传递闭包:
(1) happens-before (4); - 故
(4)读取data时保证看到(1)写入的 42。
3. 理论推导与原理解析
3.1 为什么需要内存序
考虑以下经典示例(“消息发布”模式):
std::atomic<int> flag{0};
int message = 0;
// 线程 A
message = 42;
flag.store(1, std::memory_order_relaxed);
// 线程 B
while (flag.load(std::memory_order_relaxed) != 1);
print(message); // 期望 42,但可能输出 0!
问题分析:memory_order_relaxed 不建立 happens-before 关系,CPU 与编译器可重排指令。在弱内存模型架构(ARM、POWER)上,线程 A 的 message = 42 可能被重排到 flag.store 之后,导致线程 B 看到 flag=1 但 message 仍为 0。
修复:使用 release-acquire 配对:
// 线程 A
message = 42;
flag.store(1, std::memory_order_release); // release
// 线程 B
while (flag.load(std::memory_order_acquire) != 1); // acquire
print(message); // 保证看到 42
3.2 CPU 内存模型对比
3.2.1 x86-TSO(Total Store Order)
x86 采用 TSO 模型,比 SC 弱化一点点:
- 允许 store-load 重排(写缓冲导致);
- 不允许 store-store、load-load、load-store 重排。
形式化:x86 程序在 SC 语义下,仅当出现 store-load 序对时可能观察不一致。
# x86-TSO 允许的重排
mov [x], 1 ; store
mov eax, [y] ; load
# 可能被重排为:先 load [y],再 store [x]
3.2.2 ARM/POWER(Relaxed Memory Model)
ARM 与 POWER 是更弱的内存模型:
- 允许 store-store、load-load、store-load、load-store 全部重排;
- 需要显式屏障指令(
dmbon ARM,lwsync/syncon POWER)。
# ARM relaxed model
str r1, [x] ; store
ldr r2, [y] ; load
# 无屏障时可能任意重排
dmb ish ; 数据内存屏障(inner shareable)
3.2.3 内存序在不同架构上的成本
| 内存序 | x86 编译为 | ARM 编译为 | 成本(相对) |
|---|---|---|---|
relaxed | 普通 mov | 普通 ldr/str | 1x |
acquire(load) | 普通 mov | ldr + dmb ishld | x86: 1x; ARM: 2-3x |
release(store) | 普通 mov | dmb ish + str | x86: 1x; ARM: 2-3x |
seq_cst(load) | mov + mfence 或 lock xadd | ldr + dmb ish | x86: 5-10x; ARM: 2-3x |
seq_cst(store) | xchg 或 mov + mfence | dmb ish + str + dmb ish | x86: 5-10x; ARM: 3-5x |
关键洞察:在 x86 上,acquire/release 几乎免费,但 seq_cst 显著更贵。在 ARM 上,所有非 relaxed 内存序都有显著开销,但仍比 seq_cst 便宜。
3.3 CAS(Compare-Exchange-Swap)原理
compare_exchange_weak/strong 是原子操作的核心原语。其语义:
bool compare_exchange(expected, desired):
if (*this == expected) {
*this = desired;
return true;
} else {
expected = *this;
return false;
}
形式化:
CAS 支持 “双内存序”:成功时使用 success_order,失败时使用 failure_order(failure_order 不能强于 success_order)。
std::atomic<int> a{0};
int expected = 0;
bool ok = a.compare_exchange_strong(
expected, 1,
std::memory_order_acq_rel, // 成功时
std::memory_order_acquire // 失败时
);
3.4 ABA 问题
CAS 检查值相等即认为 “无变化”,但值可能经历过 A→B→A 的变化:
// 线程 1 读到 head = A,准备 CAS(A, B)
// 线程 2 pop A, push B, push A(A 被复用)
// 线程 1 CAS(A, B) 成功,但中间状态已被破坏
解决方案:
- 带版本号的指针:
std::atomic<uintptr_t>高位存版本,低位存指针; std::atomic<std::shared_ptr>(C++20):自动管理引用计数,避免 ABA;- Hazard pointer(C++26 草案 P2530):线程持引用期间防止回收;
- Epoch-based reclamation:分代回收,类似 RCU;
std::rcu(C++26 草案 P2545):RCU 机制标准化。
3.5 缓存行与 False Sharing
CPU 缓存以缓存行(cache line)为单位(通常 64 字节)。若两个线程频繁修改位于同一缓存行的不同变量,会导致缓存行在核心间频繁迁移,性能严重退化。
struct Bad {
std::atomic<int> a; // 线程 1 频繁写
std::atomic<int> b; // 线程 2 频繁写
}; // a 与 b 大概率在同一缓存行 → false sharing
解决方案:使用 alignas(64) 对齐到缓存行:
struct Good {
alignas(64) std::atomic<int> a;
alignas(64) std::atomic<int> b;
}; // a 与 b 在不同缓存行,无 false sharing
3.6 公平性与无锁性等级
按 Herlihy-Shavit 分类:
- wait-free:每个操作在有限步内完成(最强);
- lock-free:至少一个线程在有限步内完成进度;
- obstruction-free:单线程独占执行时可在有限步内完成;
- blocking:使用互斥锁,被抢占则阻塞。
形式化:
4. 代码示例(企业级 production-ready)
4.1 自旋锁实现
// file: spinlock.cpp
// compile: g++ -std=c++17 -O2 -pthread -o spinlock spinlock.cpp
#include <atomic>
#include <thread>
#include <iostream>
#include <chrono>
class SpinLock {
std::atomic<bool> locked_{false};
public:
void lock() {
// 第一阶段:自旋等待
while (locked_.exchange(true, std::memory_order_acquire)) {
// 第二阶段:使用 pause 减少功耗(x86)
#if defined(__x86_64__) || defined(_M_X64)
__builtin_ia32_pause();
#endif
}
}
void unlock() {
locked_.store(false, std::memory_order_release);
}
bool try_lock() {
return !locked_.exchange(true, std::memory_order_acquire);
}
};
int main() {
SpinLock lock;
int counter = 0;
auto work = [&]() {
for (int i = 0; i < 1'000'000; ++i) {
std::lock_guard<SpinLock> lk(lock);
++counter;
}
};
std::thread t1(work);
std::thread t2(work);
t1.join();
t2.join();
std::cout << "counter = " << counter << "\n"; // 期望 2000000
return 0;
}
4.2 生产者-消费者(无锁 SPSC 队列)
// file: spsc_queue.cpp
// compile: g++ -std=c++17 -O2 -pthread -o spsc spsc_queue.cpp
#include <atomic>
#include <cstddef>
#include <new>
#include <thread>
#include <iostream>
template <typename T, size_t Capacity>
class SPSCQueue {
alignas(64) std::atomic<size_t> write_pos_{0};
alignas(64) std::atomic<size_t> read_pos_{0};
alignas(64) T buffer_[Capacity];
public:
bool push(const T& value) {
size_t wp = write_pos_.load(std::memory_order_relaxed);
size_t rp = read_pos_.load(std::memory_order_acquire);
if (wp - rp >= Capacity) return false; // 满
buffer_[wp % Capacity] = value;
write_pos_.store(wp + 1, std::memory_order_release);
return true;
}
bool pop(T& out) {
size_t rp = read_pos_.load(std::memory_order_relaxed);
size_t wp = write_pos_.load(std::memory_order_acquire);
if (rp == wp) return false; // 空
out = buffer_[rp % Capacity];
read_pos_.store(rp + 1, std::memory_order_release);
return true;
}
};
int main() {
SPSCQueue<int, 1024> queue;
constexpr int N = 1'000'000;
int sum = 0;
std::thread producer([&]() {
for (int i = 0; i < N; ++i) {
while (!queue.push(i)) std::this_thread::yield();
}
});
std::thread consumer([&]() {
for (int i = 0; i < N; ++i) {
int v;
while (!queue.pop(v)) std::this_thread::yield();
sum += v;
}
});
producer.join();
consumer.join();
std::cout << "sum = " << sum << "\n"; // 期望 499999500000
return 0;
}
4.3 无锁栈(带 ABA 解决方案)
// file: lockfree_stack.cpp
// compile: g++ -std=c++20 -O2 -pthread -o lfstack lockfree_stack.cpp
#include <atomic>
#include <memory>
#include <iostream>
#include <thread>
template <typename T>
class LockFreeStack {
struct Node {
std::shared_ptr<T> data;
Node* next;
Node(T x) : data(std::make_shared<T>(std::move(x))), next(nullptr) {}
};
std::atomic<Node*> head_{nullptr};
public:
void push(T value) {
Node* new_node = new Node(std::move(value));
new_node->next = head_.load(std::memory_order_relaxed);
while (!head_.compare_exchange_weak(
new_node->next, new_node,
std::memory_order_release,
std::memory_order_relaxed)) {
// CAS 失败时 new_node->next 自动更新
}
}
std::shared_ptr<T> pop() {
Node* old_head = head_.load(std::memory_order_relaxed);
while (old_head &&
!head_.compare_exchange_weak(
old_head, old_head->next,
std::memory_order_acquire,
std::memory_order_relaxed)) {
// CAS 失败时 old_head 自动更新
}
if (!old_head) return nullptr;
std::shared_ptr<T> result = old_head->data;
// 注意:直接 delete old_head 可能引发 ABA
// 生产环境应使用 hazard pointer 或 RCU
delete old_head;
return result;
}
~LockFreeStack() {
while (pop()) {}
}
};
int main() {
LockFreeStack<int> stack;
constexpr int N = 100'000;
std::thread t1([&]() { for (int i = 0; i < N; ++i) stack.push(i); });
std::thread t2([&]() { for (int i = 0; i < N; ++i) stack.push(i + N); });
t1.join();
t2.join();
int count = 0;
while (stack.pop()) ++count;
std::cout << "popped: " << count << "\n"; // 期望 200000
return 0;
}
4.4 双检查锁定(Double-Checked Locking)
// file: dclp.cpp
// compile: g++ -std=c++17 -O2 -pthread -o dclp dclp.cpp
#include <atomic>
#include <mutex>
#include <iostream>
#include <memory>
class Singleton {
static std::atomic<Singleton*> instance_;
static std::mutex mutex_;
int data_;
Singleton() : data_(42) {
std::cout << "Singleton constructed\n";
}
public:
static Singleton* getInstance() {
Singleton* p = instance_.load(std::memory_order_acquire);
if (!p) {
std::lock_guard<std::mutex> lk(mutex_);
p = instance_.load(std::memory_order_relaxed);
if (!p) {
p = new Singleton();
instance_.store(p, std::memory_order_release);
}
}
return p;
}
int getData() const { return data_; }
};
std::atomic<Singleton*> Singleton::instance_{nullptr};
std::mutex Singleton::mutex_;
int main() {
auto* s1 = Singleton::getInstance();
auto* s2 = Singleton::getInstance();
std::cout << "s1 == s2: " << (s1 == s2) << "\n";
std::cout << "data: " << s1->getData() << "\n";
return 0;
}
4.5 false sharing 演示与修复
// file: false_sharing.cpp
// compile: g++ -std=c++17 -O2 -pthread -o fsh false_sharing.cpp
#include <atomic>
#include <thread>
#include <chrono>
#include <iostream>
#include <cstdint>
// 错误版本:两个计数器在同一缓存行
struct BadCounter {
std::atomic<uint64_t> a{0};
std::atomic<uint64_t> b{0};
};
// 修复版本:使用 alignas 隔离到不同缓存行
struct alignas(64) GoodCounter {
std::atomic<uint64_t> a{0};
std::atomic<uint64_t> b{0};
};
template <typename Counter>
void benchmark(const char* name) {
Counter c;
constexpr int N = 100'000'000;
auto t0 = std::chrono::steady_clock::now();
std::thread t1([&]() { for (int i = 0; i < N; ++i) c.a.fetch_add(1, std::memory_order_relaxed); });
std::thread t2([&]() { for (int i = 0; i < N; ++i) c.b.fetch_add(1, std::memory_order_relaxed); });
t1.join();
t2.join();
auto t1_end = std::chrono::steady_clock::now();
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(t1_end - t0).count();
std::cout << name << ": " << ms << " ms, a=" << c.a.load() << " b=" << c.b.load() << "\n";
}
int main() {
benchmark<BadCounter>("BadCounter (false sharing)");
benchmark<GoodCounter>("GoodCounter (cache aligned)");
return 0;
}
典型输出(x86-64, 4 cores):
BadCounter (false sharing): 1247 ms, a=100000000 b=100000000
GoodCounter (cache aligned): 412 ms, a=100000000 b=100000000
修复后性能提升约 3 倍。
4.6 CMake 项目示例
# CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
project(lockfree_demo CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Threads REQUIRED)
add_executable(spinlock spinlock.cpp)
add_executable(spsc_queue spsc_queue.cpp)
add_executable(lockfree_stack lockfree_stack.cpp)
add_executable(dclp dclp.cpp)
add_executable(false_sharing false_sharing.cpp)
foreach(target spinlock spsc_queue lockfree_stack dclp false_sharing)
target_link_libraries(${target} PRIVATE Threads::Threads)
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(${target} PRIVATE -O2 -Wall -Wextra -fsanitize=address,undefined)
target_link_options(${target} PRIVATE -fsanitize=address,undefined)
endif()
endforeach()
# ThreadSanitizer 目标(专门用于检测数据竞争)
add_executable(spinlock_tsan spinlock.cpp)
target_compile_options(spinlock_tsan PRIVATE -O1 -fsanitize=thread)
target_link_options(spinlock_tsan PRIVATE -fsanitize=thread)
4.7 fetch_add 实现 Armstrong 数计算
// file: armstrong.cpp
// 演示 fetch_add 的常见用法
#include <atomic>
#include <vector>
#include <thread>
#include <iostream>
#include <cmath>
bool isArmstrong(int n) {
int original = n, sum = 0, digits = 0;
int temp = n;
while (temp) { digits++; temp /= 10; }
temp = n;
while (temp) {
int d = temp % 10;
sum += std::pow(d, digits);
temp /= 10;
}
return sum == original;
}
int main() {
constexpr int N = 1'000'000;
std::atomic<int> count{0};
auto worker = [&](int start, int end) {
int local_count = 0;
for (int i = start; i < end; ++i) {
if (isArmstrong(i)) ++local_count;
}
count.fetch_add(local_count, std::memory_order_relaxed);
};
int num_threads = std::thread::hardware_concurrency();
std::vector<std::thread> threads;
int chunk = N / num_threads;
for (int i = 0; i < num_threads; ++i) {
int start = i * chunk;
int end = (i == num_threads - 1) ? N : start + chunk;
threads.emplace_back(worker, start, end);
}
for (auto& t : threads) t.join();
std::cout << "Armstrong numbers < " << N << ": " << count << "\n";
// 输出: Armstrong numbers < 1000000: 16
return 0;
}
5. 对比分析
5.1 与 Rust 内存模型对比
| 维度 | C++ | Rust |
|---|---|---|
| 内存模型 | C++11 | 借鉴 C++11 与 JMM |
| 原子 API | std::atomic<T> | std::sync::atomic::* |
| 内存序数量 | 6 种 | 5 种(无 consume) |
| Unsafe 检查 | 无 | unsafe 块要求 |
| 数据竞争 UB | UB(C++20 起可被 sanitizer 检测) | 编译期禁止(borrow checker) |
| 无锁库 | 自实现 / boost::lockfree | crossbeam、tokio |
| cache line padding | alignas(64) | #[repr(align(64))] |
5.2 与 Java JMM 对比
| 维度 | C++ | Java |
|---|---|---|
| 内存模型 | C++11 | JMM (JSR-133) |
volatile 语义 | 无(C++ volatile 与并发无关) | 类似 seq_cst 原子 |
| 原子 API | std::atomic<T> | AtomicInteger 等 |
| Happens-before | 形式化定义 | 形式化定义 |
| final 字段保证 | 无 | final 字段初始化后对所有线程可见 |
| 异常安全 | 移动语义 + noexcept | try-catch + finally |
5.3 与 Go 内存模型对比
| 维度 | C++ | Go |
|---|---|---|
| 内存模型 | C++11 (2011) | Go MM (2009, 修订 2022) |
| 原子操作 | std::atomic<T> | sync/atomic 包 |
| 内存序选项 | 6 种 | 3 种(SeqCst/Acquire/Release) |
| 协程支持 | std::thread、std::coroutine(C++20) | goroutine 内置 |
| Channel 通信 | 无内置 | chan 内置(CSP 模型) |
| Race Detector | TSan | go test -race |
5.4 6 种内存序性能对比
| 内存序 | x86 编译开销 | ARM 编译开销 | 适用场景 |
|---|---|---|---|
relaxed | 无(普通 mov) | 无(普通 ldr/str) | 计数器、统计 |
consume | 通常退化为 acquire | 类似 acquire | 数据依赖场景(少用) |
acquire(load) | 无(普通 mov) | dmb ishld | 与 release 配对 |
release(store) | 无(普通 mov) | dmb ish + str | 与 acquire 配对 |
acq_rel(RMW) | 普通 lock xadd | ldaxr/stlxr + dmb | RMW 操作 |
seq_cst(load) | mfence 或 lock | dmb ish | 全局总序需求 |
seq_cst(store) | xchg 或 mov + mfence | dmb ish + str + dmb ish | 全局总序需求 |
经验法则:默认 seq_cst,性能敏感时降为 acquire/release,纯计数器用 relaxed。
6. 常见陷阱与最佳实践
6.1 陷阱 1:误用 memory_order_relaxed
std::atomic<bool> ready{false};
int data = 0;
// 线程 A
data = 42;
ready.store(true, std::memory_order_relaxed); // 错误!
// 线程 B
while (!ready.load(std::memory_order_relaxed));
print(data); // 不保证看到 42
最佳实践:消息发布场景必须使用 release/acquire 配对。
6.2 陷阱 2:忘记 atomic 之外的内存访问
// 错误:data 不是 atomic
int data = 0;
bool flag = false; // 非 atomic
// 线程 A
data = 42;
flag = true; // 编译器/CPU 可能重排
// 线程 B
while (!flag);
print(data); // UB:数据竞争
最佳实践:跨线程通信变量必须 std::atomic。
6.3 陷阱 3:CAS 中的 ABA 问题
// 简化的无锁栈 pop
Node* old_head = head.load();
while (old_head && !head.compare_exchange_weak(old_head, old_head->next));
delete old_head;
// 问题:old_head 可能在 CAS 期间被 pop 后又 push 回来(A→B→A)
// 导致 old_head->next 已失效
解决方案:
- 使用
std::atomic<std::shared_ptr<Node>>(C++20); - 带版本号指针(DCAS 模拟);
- Hazard pointer(C++26);
- Epoch-based reclamation。
6.4 陷阱 4:compare_exchange_weak vs strong
// weak 版本可能"伪失败"(实际值等于 expected 但仍返回 false)
while (!head.compare_exchange_weak(expected, desired));
// 强版本不会伪失败,但循环场景仍推荐 weak(性能更优)
if (head.compare_exchange_strong(expected, desired)) { ... }
最佳实践:循环场景用 weak,单次判断用 strong。
6.5 陷阱 5:忘记 false sharing
struct Bad {
std::atomic<int> a, b, c, d; // 大概率在同一缓存行
};
最佳实践:高频访问的原子变量使用 alignas(64)。
6.6 陷阱 6:误用 volatile
volatile bool ready = false; // C++ volatile 不保证原子性!
// 线程 A: ready = true;
// 线程 B: while (!ready);
// 这是 UB:数据竞争
最佳实践:跨线程通信使用 std::atomic,不要用 volatile。
6.7 陷阱 7:内存序不匹配
// 线程 A: data = 42; flag.store(true, release);
// 线程 B: while (!flag.load(relaxed)); print(data); // 错误:acquire 缺失
最佳实践:release 必须配对 acquire,否则不建立 synchronizes-with 关系。
6.8 陷阱 8:fence 与 atomic 操作混淆
// 错误:fence 不能单独起作用,必须配对原子操作
std::atomic_thread_fence(std::memory_order_release);
// 还需 store 一个原子变量才能建立同步
最佳实践:fence 与一个原子操作配对使用,建立 release-acquire 关系。
6.9 UB 清单
| UB 类型 | 描述 | 检测方法 |
|---|---|---|
| 数据竞争 | 非 atomic 变量跨线程读写 | TSan |
| ABA 问题 | CAS 通过但中间状态已变 | Hazard pointer / shared_ptr |
| 错误的内存序 | 缺少 acquire/release | 代码审查 + TSan |
| False sharing | 性能退化(非 UB) | perf c2c |
| 移动后语义假设 | 假设 atomic 移动后值 | UBSan |
volatile 误用 | 跨线程非 atomic | TSan |
6.10 最佳实践清单
- 默认
seq_cst:除非性能测试证明需要降低。 - 优先 atomic:跨线程通信的变量必须是
std::atomic。 - 配对使用:release 必须与 acquire 配对。
- 避免 false sharing:
alignas(64)隔离高频原子变量。 - 慎用 fence:除非有明确理由,优先用 atomic 内置内存序。
- 测试时启用 TSan:
-fsanitize=thread检测数据竞争。 - 使用现成无锁库:如
boost::lockfree、moodycamel::ConcurrentQueue。 - 慎用 consume:C++17 起 deprecated,C++23 重新规范但仍少用。
7. 工程实践
7.1 构建与依赖
cmake_minimum_required(VERSION 3.20)
project(my_concurrent_lib CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Threads REQUIRED)
find_package(Boost REQUIRED COMPONENTS lockfree)
add_library(my_concurrent STATIC
src/queue.cpp
src/stack.cpp
)
target_link_libraries(my_concurrent PUBLIC Threads::Threads Boost::lockfree)
# 启用 ThreadSanitizer(仅 Debug)
option(ENABLE_TSAN "Enable ThreadSanitizer" OFF)
if(ENABLE_TSAN)
add_compile_options(-fsanitize=thread -g)
add_link_options(-fsanitize=thread)
endif()
7.2 性能基准测试
// file: bench_atomic.cpp
// compile: g++ -std=c++20 -O2 -pthread -o bench bench_atomic.cpp -lbenchmark
#include <benchmark/benchmark.h>
#include <atomic>
#include <thread>
#include <vector>
static void BM_AtomicRelaxed(benchmark::State& state) {
std::atomic<int> counter{0};
for (auto _ : state) {
counter.fetch_add(1, std::memory_order_relaxed);
}
}
BENCHMARK(BM_AtomicRelaxed)->Threads(1)->Threads(4)->Threads(8);
static void BM_AtomicSeqCst(benchmark::State& state) {
std::atomic<int> counter{0};
for (auto _ : state) {
counter.fetch_add(1, std::memory_order_seq_cst);
}
}
BENCHMARK(BM_AtomicSeqCst)->Threads(1)->Threads(4)->Threads(8);
static void BM_FalseSharing(benchmark::State& state) {
struct Bad { std::atomic<int> a, b; };
Bad bad;
for (auto _ : state) {
bad.a.fetch_add(1, std::memory_order_relaxed);
bad.b.fetch_add(1, std::memory_order_relaxed);
}
}
BENCHMARK(BM_FalseSharing)->Threads(2);
static void BM_NoFalseSharing(benchmark::State& state) {
struct Good {
alignas(64) std::atomic<int> a;
alignas(64) std::atomic<int> b;
};
Good good;
for (auto _ : state) {
good.a.fetch_add(1, std::memory_order_relaxed);
good.b.fetch_add(1, std::memory_order_relaxed);
}
}
BENCHMARK(BM_NoFalseSharing)->Threads(2);
BENCHMARK_MAIN();
7.3 调试技巧
1. ThreadSanitizer(TSan)检测数据竞争:
g++ -std=c++20 -O1 -g -fsanitize=thread -fno-omit-frame-pointer \
-o spinlock spinlock.cpp -pthread
./spinlock
# 若有数据竞争,TSan 会输出详细报告
2. AddressSanitizer + UBSan 检测内存错误:
g++ -std=c++20 -O1 -g -fsanitize=address,undefined \
-o spinlock spinlock.cpp -pthread
3. perf c2c 检测 false sharing:
perf c2c record ./bench
perf c2c report
4. 使用 std::atomic::is_always_lock_free:
static_assert(std::atomic<int>::is_always_lock_free);
// C++17 起保证某些原子操作是无锁的
7.4 编译器内置原子操作
GCC/Clang 提供 __atomic_* 内置函数:
int x = 0;
__atomic_store_n(&x, 42, __ATOMIC_RELEASE);
int v = __atomic_load_n(&x, __ATOMIC_ACQUIRE);
bool ok = __atomic_compare_exchange_n(&x, &expected, desired,
false, // weak
__ATOMIC_ACQ_REL,
__ATOMIC_ACQUIRE);
在 C 代码或非 C++ 标准库环境下可使用。
7.5 跨平台注意事项
// Windows 平台使用 Interlocked API
#include <windows.h>
LONG old = InterlockedExchangeAdd(&counter, 1);
// Linux 平台使用 __sync_* 或 stdatomic.h
#include <stdatomic.h>
atomic_fetch_add(&counter, 1);
// macOS 平台使用 OSAtomic
#include <libkern/OSAtomic.h>
int32_t old = OSAtomicAdd32(1, &counter);
7.6 CI/CD 集成
# .github/workflows/concurrent-ci.yml
name: Concurrent CI
on: [push, pull_request]
jobs:
tsan:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- run: |
sudo apt-get install -y clang-17 cmake ninja-build
cmake -B build -G Ninja -DCMAKE_CXX_COMPILER=clang++-17 \
-DENABLE_TSAN=ON
cmake --build build
cd build && ctest --output-on-failure
asan:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- run: |
sudo apt-get install -y g++-13 cmake ninja-build
cmake -B build -G Ninja -DCMAKE_CXX_COMPILER=g++-13 \
-DCMAKE_BUILD_TYPE=Debug
cmake --build build
cd build && ctest --output-on-failure
8. 案例研究
8.1 案例一:Linux 内核 READ_ONCE / WRITE_ONCE
Linux 内核使用 READ_ONCE(x) 和 WRITE_ONCE(x, v) 替代普通访问,对应 C++ 的 atomic.load(relaxed) 与 atomic.store(v, relaxed):
// Linux 内核
int x;
WRITE_ONCE(x, 42); // 类似 atomic_store(&x, 42, relaxed)
int v = READ_ONCE(x); // 类似 atomic_load(&x, relaxed)
Linux 内存模型(LKMM)与 C++11 模型有细微差异,但基本概念一致。
8.2 案例二:Folly MicroLock
Facebook Folly 库的 MicroLock 通过位运算在单个 uint8_t 中存储 8 个锁状态,最大化缓存利用:
class MicroLockCore {
uint8_t lock_byte_;
public:
void lock(int idx) {
uint8_t mask = 1 << idx;
while (__builtin_popcount(lock_byte_ & mask) ||
!__sync_bool_compare_and_swap(&lock_byte_, lock_byte_, lock_byte_ | mask)) {
// 自旋
}
}
};
8.3 案例三:Intel TBB concurrent_vector
Intel TBB 的 concurrent_vector 通过分段增长(segmented growth)实现并发安全的增长操作:
- 每段独立分配,避免全局锁;
- 索引通过段号 + 段内偏移计算;
- 增长时仅锁当前段。
8.4 案例四:Java ConcurrentHashMap
Java 的 ConcurrentHashMap 使用分段锁(Java 7)或 CAS + synchronized(Java 8+):
// Java 8+ 风格
Node<K,V> f; int n;
if ((f = tabAt(tab, i)) == null) {
if (casTabAt(tab, i, null, new Node<>(...))) break;
} else {
synchronized (f) { /* 链表插入 */ }
}
C++ 等价实现可参考 folly::AtomicHashMap 或 tbb::concurrent_hash_map。
8.5 案例五:DPDK 无锁环形缓冲
DPDK(Data Plane Development Kit)的 rte_ring 是高性能无锁环形队列,支持单生产者-单消费者(SPSC)和多生产者-多消费者(MPMC)模式:
// DPDK rte_ring 简化
struct rte_ring {
struct rte_ring_head tail;
struct rte_ring_head head;
void* buffer[capacity];
};
int rte_ring_enqueue(struct rte_ring* r, void* obj) {
uint32_t prod_head = r->head.prod;
uint32_t prod_next = prod_head + 1;
if (prod_next - r->tail.cons >= capacity) return -ENOBUFS;
if (!__sync_bool_compare_and_swap(&r->head.prod, prod_head, prod_next))
return -ENOBUFS; // 重试
r->buffer[prod_head & mask] = obj;
__sync_synchronize();
return 0;
}
填空题知识点讲解
常见疑问 5:. C++11 定义了 ______ 种 std::memory_order。
6 种:relaxed, consume, acquire, release, acq_rel, seq_cst。
常见疑问 6:. std::atomic<T> 默认内存序是 ______。
seq_cst。
常见疑问 7:. x86 上 mfence 指令对应 C++ 的 ______ 内存序。
seq_cst。
常见疑问 8:. 缓存行通常大小为 ______ 字节,可通过 ______ 关键字对齐。
64;alignas(64)。
编程题知识点讲解
常见疑问 9:. 实现一个无锁自旋锁 SpinLock,使用 acquire/release 内存序。
#include <atomic>
class SpinLock {
std::atomic_flag flag_ = ATOMIC_FLAG_INIT;
public:
void lock() {
while (flag_.test_and_set(std::memory_order_acquire)) {
// 自旋
}
}
void unlock() {
flag_.clear(std::memory_order_release);
}
};
常见疑问 10:. 实现一个简单的 MPMCCounter,允许多线程递增,最终读取总数。
#include <atomic>
class MPMCCounter {
std::atomic<long> count_{0};
public:
void increment() {
count_.fetch_add(1, std::memory_order_relaxed);
}
long get() const {
return count_.load(std::memory_order_acquire);
}
};
常见疑问 11:. 实现一个简化版的 LockFreeQueue<T>(单生产者单消费者)。
#include <atomic>
#include <cstddef>
template <typename T, size_t Capacity>
class SPSCQueue {
alignas(64) std::atomic<size_t> write_pos_{0};
alignas(64) std::atomic<size_t> read_pos_{0};
T buffer_[Capacity];
public:
bool push(T value) {
size_t wp = write_pos_.load(std::memory_order_relaxed);
size_t rp = read_pos_.load(std::memory_order_acquire);
if (wp - rp >= Capacity) return false;
buffer_[wp % Capacity] = std::move(value);
write_pos_.store(wp + 1, std::memory_order_release);
return true;
}
bool pop(T& out) {
size_t rp = read_pos_.load(std::memory_order_relaxed);
size_t wp = write_pos_.load(std::memory_order_acquire);
if (rp == wp) return false;
out = std::move(buffer_[rp % Capacity]);
read_pos_.store(rp + 1, std::memory_order_release);
return true;
}
};
11.1 书籍
- 《C++ Concurrency in Action》(Anthony Williams, 2nd ed., 2019):C++ 并发权威著作,详细讲解内存模型与无锁编程。
- 《The Art of Multiprocessor Programming》(Maurice Herlihy, Nir Shavit, 2nd ed., 2012):无锁算法理论经典。
- 《Concurrency in Practice》(Brian Goetz et al., 2006):Java 视角,但内存模型概念通用。
- 《Java Concurrency in Practice》(Brian Goetz, 2006):JMM 详解。
- 《Programming with POSIX Threads》(David R. Butenhof, 1997):低层线程 API。
11.2 论文与提案
- N2007: Memory Model for C++ (Boehm, 2005) — C++ 内存模型奠基。
- N2348: A Strong and Safe Memory Model for C++ (Boehm, 2007) — 最终版本。
- P0019: atomic_ref (Halpern, 2017) — 提案于 2017 年提交,随 C++20 正式纳入标准。
- P1135: The C++20 Synchronization Library — semaphore/latch/barrier。
- P2530: Hazard Pointers for C++26 — hazard pointer 标准化。
- P2545: Read-Copy-Update (RCU) for C++26 — RCU 标准化。
- P2045: atomic_ref
— 浮点原子操作。
11.4 视频课程
- CPPCon: C++ Memory Model (Herb Sutter, 2019) — 内存模型概览。
- CPPCon: Lock-Free Programming (Fedor Pikus, 2018) — 无锁编程实战。
- MIT 6.172: Performance Engineering of Software Systems — 多核性能优化。
- Stanford CS149: Parallel Computing — 并行计算原理。
- CMU 15-440: Distributed Systems — 分布式系统(含内存一致性模型)。
11.5 开源项目源码阅读
- boost::lockfree: https://github.com/boost-org/lockfree — 无锁队列与栈。
- Folly: https://github.com/facebook/folly — 包含
MicroLock、HazPtr等。 - Intel TBB: https://github.com/oneapi-src/oneTBB —
concurrent_vector等。 - DPDK rte_ring: https://github.com/DPDK/dpdk/tree/main/lib/ring — 高性能无锁环形缓冲。
- moodycamel ConcurrentQueue: https://github.com/cameron314/concurrentqueue — 工业级 MPMC 队列。
- Linux kernel LKMM: https://github.com/torvalds/linux/tree/master/tools/memory-model — Linux 内存模型工具。
附录 A:6 种内存序速查表
| 内存序 | load 语义 | store 语义 | RMW 语义 | 典型用途 |
|---|---|---|---|---|
relaxed | 无 | 无 | 无 | 计数器、统计 |
consume | 数据依赖 | 无 | 无 | 依赖链同步(少用) |
acquire | 后续读/写不重排到前 | — | — | 与 release 配对 load |
release | — | 之前的读/写不重排到后 | — | 与 acquire 配对 store |
acq_rel | acquire | release | acquire + release | RMW(如 exchange) |
seq_cst | acquire + 全局序 | release + 全局序 | acq_rel + 全局序 | 默认值,全局总序 |
附录 B:架构内存模型速查
| 架构 | 模型 | 允许重排 | 屏障指令 |
|---|---|---|---|
| x86 / x86-64 | TSO | store-load | mfence, lock 前缀 |
| ARMv7 | Relaxed | 全部 | dmb, dsb, isb |
| ARMv8 / AArch64 | Relaxed (with acquire/release) | 全部 | ldar, stlr |
| POWER | Relaxed | 全部 | lwsync, sync, isync |
| Alpha | Relaxed + speculative read | 全部 + speculative | mb |
| RISC-V | RVWMO | 全部 | fence |
| Itanium | Relaxed | 全部 | mf, mf.a |
附录 C:常用原子操作 API
#include <atomic>
// 基本读写
T load(memory_order = seq_cst);
void store(T, memory_order = seq_cst);
// RMW(read-modify-write)
T exchange(T, memory_order = seq_cst);
bool compare_exchange_weak(T& expected, T desired,
memory_order success, memory_order failure);
bool compare_exchange_strong(T& expected, T desired,
memory_order success, memory_order failure);
// 算术运算(仅整型与指针)
T fetch_add(T, memory_order = seq_cst);
T fetch_sub(T, memory_order = seq_cst);
T fetch_and(T, memory_order = seq_cst);
T fetch_or(T, memory_order = seq_cst);
T fetch_xor(T, memory_order = seq_cst);
// C++20 新增
bool wait(T old, memory_order = seq_cst);
void notify_one();
void notify_all();
// 类型特征
static constexpr bool is_always_lock_free;
bool is_lock_free() const noexcept;
// Fence
void atomic_thread_fence(memory_order);
void atomic_signal_fence(memory_order);
附录 D:常见错误代码模式
// 错误 1:non-atomic 跨线程读写
int data = 0;
// 线程 A: data = 42;
// 线程 B: print(data); // UB
// 错误 2:内存序不匹配
// 线程 A: data = 42; flag.store(true, release);
// 线程 B: while (!flag.load(relaxed)); // 应使用 acquire
// 错误 3:ABA 问题
// Node* old = head.load();
// while (!head.compare_exchange_weak(old, old->next));
// delete old; // 可能 dangling
// 错误 4:false sharing
// struct Bad { atomic<int> a, b; };
// 错误 5:volatile 误用
// volatile bool ready = false; // 不是 atomic
// 错误 6:fence 单独使用
// atomic_thread_fence(release); // 必须配对原子操作
// 错误 7:consume 用作 acquire
// x.load(consume) // 仅同步数据依赖,不能保证其他变量
// 错误 8:CAS 后忘记更新 expected
// while (!head.compare_exchange_weak(old, new)) { /* old 应自动更新 */ }
原子类型
基本写法:声明原子变量
std::atomic<<类型>> <变量>;
// 原子整型变量
std::atomic<int> counter{0};
基本写法:原子加载
<变量>.load([<内存序>]);
// 原子读取值
int v = counter.load(std::memory_order_acquire);
基本写法:原子存储
<变量>.store(<值>, [<内存序>]);
// 原子写入值
counter.store(10, std::memory_order_release);
基本写法:原子交换
<变量>.exchange(<值>, [<内存序>]);
// 原子替换并返回旧值
int old = counter.exchange(5);
CAS 操作
基本写法:比较并交换
<变量>.compare_exchange_strong(<期望>, <新值>, [<内存序>]);
// 强版本 CAS,失败时更新期望值
int expected = 0;
bool ok = counter.compare_exchange_strong(expected, 1);
基本写法:弱版本 CAS
<变量>.compare_exchange_weak(<期望>, <新值>);
// 可能伪失败,适合循环中
while (!counter.compare_exchange_weak(expected, expected + 1));
基本写法:fetch_add 原子加法
<变量>.fetch_add(<值>, [<内存序>]);
// 原子加并返回旧值
int prev = counter.fetch_add(1);
基本写法:fetch_sub 原子减法
<变量>.fetch_sub(<值>, [<内存序>]);
// 原子减并返回旧值
int prev = counter.fetch_sub(1);
内存序
基本写法:顺序一致性
std::memory_order_seq_cst
// 最强保证,全局总序
counter.store(1, std::memory_order_seq_cst);
基本写法:获取语义
std::memory_order_acquire
// 加载时保证后续读不重排到此之前
v = counter.load(std::memory_order_acquire);
基本写法:释放语义
std::memory_order_release
// 存储时保证之前写不重排到此之后
counter.store(1, std::memory_order_release);
基本写法:宽松语义
std::memory_order_relaxed
// 仅保证原子性无顺序约束
counter.fetch_add(1, std::memory_order_relaxed);
fence 屏障
基本写法:释放屏障
std::atomic_thread_fence(std::memory_order_release);
// 显式内存屏障防止写重排
std::atomic_thread_fence(std::memory_order_release);
data = 42;
ready.store(true);
基本写法:获取屏障
std::atomic_thread_fence(std::memory_order_acquire);
// 显式内存屏障防止读重排
std::atomic_thread_fence(std::memory_order_acquire);
int v = data;
自旋锁示例
基本写法:使用原子实现自旋锁
while (<锁>.test_and_set(std::memory_order_acquire)) {}
// 原子标志位自旋等待
std::atomic_flag lock = ATOMIC_FLAG_INIT;
while (lock.test_and_set(std::memory_order_acquire)) {}
// 临界区
lock.clear(std::memory_order_release);
基本写法:等待与通知
<变量>.wait(<旧值>); <变量>.notify_one();
// C++20 原子等待通知
counter.wait(0); // 阻塞直到值变化
counter.store(1);
counter.notify_one(); // 唤醒一个等待者