4. 并发写入控制:读写锁、无锁队列、CAS操作、内存屏障
好,咱们进入正题。订单簿这东西,说白了就是个高频战场。多个线程同时往里写数据——有人下单,有人撤单,有人改单。你要是控制不好并发,轻则数据错乱,重则直接崩盘。我当年刚做量化系统时,就吃过这个亏。
今天咱们聊四个核心武器:读写锁、无锁队列、CAS操作、内存屏障。每个都是实战中摸爬滚打出来的经验。
4.1 读写锁:最朴素的并发控制
读写锁,英文叫 Read-Write Lock。它的逻辑很简单:读读不互斥,读写互斥,写写互斥。
订单簿里,大部分操作是查询(读),少部分是修改(写)。用读写锁,读线程可以并行,写线程独占。这比直接用互斥锁效率高不少。
核心原则:读多写少的场景,读写锁是首选。但别滥用——如果写操作占比超过20%,读写锁反而可能比互斥锁慢。
我见过一个项目,订单簿用读写锁,但写操作频繁到每秒几万次。结果呢?写线程抢锁,读线程饿死,延迟飙升。后来改成无锁方案才解决。
4.1.1 读写锁的实现方式
C++里,std::shared_mutex 就是读写锁。Python 的话,threading.RLock 不行,得用 readerwriterlock 库,或者自己封装。
// C++ 示例:读写锁保护订单簿
#include <shared_mutex>
class OrderBook {
private:
std::shared_mutex mtx_;
std::map<double, OrderList> bids_; // 买单
std::map<double, OrderList> asks_; // 卖单
public:
// 读操作:查询最优价格
double getBestBid() {
std::shared_lock<std::shared_mutex> lock(mtx_);
if (bids_.empty()) return 0.0;
return bids_.rbegin()->first;
}
// 写操作:添加订单
void addOrder(const Order& order) {
std::unique_lock<std::shared_mutex> lock(mtx_);
// 实际插入逻辑...
}
};
我的习惯:读锁用 shared_lock,写锁用 unique_lock。别搞混了,否则死锁等着你。
4.2 无锁队列:高性能的基石
读写锁虽然好用,但锁就是锁。有锁就有上下文切换,就有等待。在高频交易场景,微秒级的延迟都受不了。
无锁队列(Lock-Free Queue)应运而生。它基于 CAS 操作,不需要线程挂起等待。说白了,就是让多个线程同时操作一个队列,谁先抢到谁先干。
4.2.1 经典实现:MPSC 队列
订单簿里最常见的模式是:多个生产者(多个交易线程)往队列里写,一个消费者(撮合引擎)从队列里读。这就是 MPSC(Multiple Producer Single Consumer)队列。
// 简化版 MPSC 队列核心逻辑
template<typename T>
class MPSCQueue {
private:
struct Node {
T data;
std::atomic<Node*> next;
Node(const T& d) : data(d), next(nullptr) {}
};
std::atomic<Node*> head_;
std::atomic<Node*> tail_; // 消费者专用
public:
MPSCQueue() {
Node* dummy = new Node(T());
head_.store(dummy, std::memory_order_relaxed);
tail_.store(dummy, std::memory_order_relaxed);
}
// 生产者:CAS 入队
void push(const T& data) {
Node* node = new Node(data);
Node* prev = head_.exchange(node, std::memory_order_acq_rel);
prev->next.store(node, std::memory_order_release);
}
// 消费者:批量出队
bool pop(T& result) {
Node* tail = tail_.load(std::memory_order_relaxed);
Node* next = tail->next.load(std::memory_order_acquire);
if (next == nullptr) return false;
result = next->data;
tail_.store(next, std::memory_order_release);
delete tail;
return true;
}
};
我曾经踩过的坑:无锁队列的内存回收是个大问题。ABA 问题听说过吗?就是节点被回收后又重新分配,导致 CAS 误判。解决方案是用 Hazard Pointer 或 Epoch-Based Reclamation。别偷懒,否则线上事故等着你。
4.3 CAS 操作:无锁编程的原子基石
CAS 全称 Compare-And-Swap,比较并交换。它是 CPU 提供的一条原子指令。无锁队列、无锁栈、无锁哈希表,底层都靠它。
CAS 的逻辑很简单:如果内存中的值等于期望值,就把它改成新值;否则什么都不做。整个过程是原子的,不会被线程调度打断。
4.3.1 CAS 的三种变体
| 名称 | 说明 | 适用场景 |
|---|---|---|
| Compare-And-Swap | 比较并交换 | 通用场景 |
| Fetch-And-Add | 原子加 | 计数器、序列号生成 |
| Load-Link / Store-Conditional | LL/SC 对 | ARM 架构下的 CAS 替代 |
C++ 里,std::atomic 封装了 CAS。Python 的话,得用 ctypes 或者 multiprocessing 的原子操作,但说实话,Python 做无锁编程不太现实——GIL 锁在那摆着呢。
// C++ CAS 示例:原子计数器
std::atomic<int> counter{0};
void increment() {
int expected = counter.load(std::memory_order_relaxed);
while (!counter.compare_exchange_weak(
expected, expected + 1,
std::memory_order_release,
std::memory_order_relaxed)) {
// CAS 失败,expected 已被更新为最新值
// 继续重试
}
}
我建议:用 compare_exchange_weak 而不是 compare_exchange_strong。weak 版本在某些架构上性能更好,而且失败时自动重试,逻辑上没区别。
4.4 内存屏障:看不见的秩序
内存屏障(Memory Barrier),也叫内存栅栏。它是 CPU 和编译器优化的一道枷锁。
你想想看,现代 CPU 为了性能,会乱序执行指令。编译器也会重排代码。单线程下没问题,但多线程下,你写的代码顺序和实际执行顺序可能完全不同。
举个例子:
// 线程 A
data = 42;
flag = true;
// 线程 B
if (flag) {
print(data); // 可能打印出 0,而不是 42!
}
为什么会这样?因为 CPU 可能先把 flag 写入了内存,data 还在缓存里。线程 B 看到 flag 为 true,但 data 还是旧值。
解决方案就是内存屏障:
// 线程 A
data = 42;
std::atomic_thread_fence(std::memory_order_release);
flag = true;
// 线程 B
if (flag) {
std::atomic_thread_fence(std::memory_order_acquire);
print(data); // 保证看到 42
}
4.4.1 六种内存序
C++ 提供了六种内存序,从松到严:
| 内存序 | 说明 | 性能 |
|---|---|---|
| memory_order_relaxed | 无任何顺序保证 | 最快 |
| memory_order_consume | 数据依赖顺序 | 较快 |
| memory_order_acquire | 读操作后的屏障 | 中等 |
| memory_order_release | 写操作前的屏障 | 中等 |
| memory_order_acq_rel | 读+写屏障 | 较慢 |
| memory_order_seq_cst | 全局顺序一致 | 最慢 |
注意:别一上来就用 seq_cst。它最安全,但也最慢。订单簿这种高性能场景,尽量用 acquire/release 组合。我见过有人全用 seq_cst,性能直接腰斩。
4.5 知识体系总览
下面这张图,是我自己总结的并发控制知识体系。你把它存下来,以后写代码时对照着看。
4.6 实战建议
说了这么多,到底怎么选?我个人的经验是:
- 先上读写锁。简单、安全、容易调试。大部分订单簿系统,读写锁就够用了。
- 性能瓶颈了,再上无锁队列。别一开始就搞无锁,调试起来能让你怀疑人生。
- CAS 操作一定要配合内存屏障。光有 CAS 没有屏障,数据还是可能乱序。
- 内存序能松就别紧。
relaxed能解决的问题,别用acquire。省一点是一点。
一句话总结:并发控制不是越复杂越好,而是越合适越好。订单簿的核心是快,但快的前提是稳。稳不住,再快也是白搭。
公众号:蓝海资料掘金营,微信deep3321