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 知识体系总览

下面这张图,是我自己总结的并发控制知识体系。你把它存下来,以后写代码时对照着看。

并发写入控制知识体系 并发写入控制 读写锁 无锁队列 CAS操作 内存屏障 shared_mutex 读多写少场景 写占比<20% MPSC队列 CAS入队 ABA问题 compare_exchange Fetch-And-Add LL/SC对 acquire/release seq_cst最慢 乱序执行 核心原则:无锁优先,有锁兜底,内存序精确控制 性能:无锁队列 > 读写锁 > 互斥锁

4.6 实战建议

说了这么多,到底怎么选?我个人的经验是:

  1. 先上读写锁。简单、安全、容易调试。大部分订单簿系统,读写锁就够用了。
  2. 性能瓶颈了,再上无锁队列。别一开始就搞无锁,调试起来能让你怀疑人生。
  3. CAS 操作一定要配合内存屏障。光有 CAS 没有屏障,数据还是可能乱序。
  4. 内存序能松就别紧relaxed 能解决的问题,别用 acquire。省一点是一点。

一句话总结:并发控制不是越复杂越好,而是越合适越好。订单簿的核心是快,但快的前提是稳。稳不住,再快也是白搭。


公众号:蓝海资料掘金营,微信deep3321