4. 无锁数据结构:原子操作、内存序与无锁队列

好,咱们今天聊点硬核的——无锁数据结构。

说到低延迟网络编程,锁永远是个绕不开的话题。我见过太多项目,明明业务逻辑写得挺漂亮,一上压测,延迟直接炸了。查到最后,往往是某个不起眼的锁成了瓶颈。你想想看,一个线程抢到锁,其他九个线程都在那干等,CPU空转,延迟能不上去吗?

所以,无锁编程就成了一条不得不走的路。说白了,就是用原子操作代替锁,让多个线程能同时往前冲,不用互相等。

4.1 原子操作:std::atomic 的正确打开方式

原子操作,顾名思义,就是不可分割的操作。要么全部执行完,要么一点没执行。没有中间状态。

C++11 给我们带来了 std::atomic,这东西太好用了。我刚开始用的时候,觉得它就是个线程安全的变量包装器。后来踩了坑才明白,事情没那么简单。

核心要点:原子操作保证的是操作的原子性,不是内存可见性。这两个概念千万别搞混了。

来看个最简单的例子:

#include <atomic>
#include <thread>

std::atomic<int> counter{0};

void worker() {
    for (int i = 0; i < 10000; ++i) {
        counter.fetch_add(1, std::memory_order_relaxed);
    }
}

int main() {
    std::thread t1(worker);
    std::thread t2(worker);
    t1.join();
    t2.join();
    // counter 最终一定是 20000
    return 0;
}

嗯,这里要注意。用 fetch_add 而不是 counter++。为什么?因为 counter++ 实际上是三步操作:读、加、写。虽然 std::atomic 重载了 ++,但它的语义和 fetch_add 是一样的。我个人习惯直接用 fetch_add,更明确,不容易产生歧义。

4.2 内存序:别让编译器坑了你

内存序(Memory Order)是无锁编程里最容易翻车的地方。我见过太多人,原子操作用得飞起,内存序随便写个 memory_order_relaxed,结果程序跑起来行为诡异,查都查不到原因。

C++ 定义了六种内存序,但常用的就三种:

内存序 含义 使用场景
memory_order_relaxed 只保证原子性,不保证顺序 计数器、统计量
memory_order_acquire 保证后续读操作不会被重排到前面 读取锁状态、读取生产者数据
memory_order_release 保证前面的写操作不会被重排到后面 写入锁状态、写入生产者数据
memory_order_acq_rel acquire + release 读-改-写操作
memory_order_seq_cst 全局顺序一致(默认) 通用场景,但性能最差

我曾经在一个高频交易系统里,用 memory_order_relaxed 实现了一个共享计数器。本地测试一切正常,一上生产环境,数据就对不上了。查了两天才发现,是 CPU 重排了指令。从那以后,我对内存序就再也不敢马虎了。

我的建议:如果你不确定该用哪个,先用默认的 memory_order_seq_cst。等性能瓶颈出现了,再针对性地优化。别一开始就追求极致性能,正确性永远是第一位的。

4.3 无锁队列:从理论到实践

无锁队列是无锁数据结构里最经典的应用。说白了,就是一个队列,多个线程可以同时往里塞数据、往外取数据,不需要加锁。

这里我画了一张图,帮你理解无锁队列的核心逻辑:

无锁队列核心逻辑 数据1 数据2 数据3 head tail 生产者线程 CAS(tail, 空, 新数据) 消费者线程 CAS(head, 数据, 空) 核心思想:用 CAS 原子操作替代锁,实现无等待的入队和出队 head 指向第一个有效数据,tail 指向下一个空闲位置 多个生产者/消费者同时操作时,通过 CAS 保证只有一个成功

无锁队列的核心就是 CAS(Compare-And-Swap)操作。CAS 做的事情很简单:比较某个内存位置的值是否等于预期值,如果是,就把它改成新值。整个过程是原子的。

来看一个经典的无锁队列实现:

template<typename T>
class LockFreeQueue {
private:
    struct Node {
        std::shared_ptr<T> data;
        std::atomic<Node*> next;
        Node() : next(nullptr) {}
    };
    
    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);
    }
    
    void push(T value) {
        Node* new_node = new Node();
        new_node->data = std::make_shared<T>(std::move(value));
        
        while (true) {
            Node* last = tail.load(std::memory_order_acquire);
            Node* next = last->next.load(std::memory_order_acquire);
            
            if (last == tail.load(std::memory_order_acquire)) {
                if (next == nullptr) {
                    // 尝试将新节点链接到尾部
                    if (last->next.compare_exchange_weak(
                            next, new_node,
                            std::memory_order_release,
                            std::memory_order_relaxed)) {
                        // 更新 tail 指针
                        tail.compare_exchange_strong(
                            last, new_node,
                            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);
                }
            }
        }
    }
    
    std::shared_ptr<T> pop() {
        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 nullptr;  // 队列为空
                    }
                    // tail 落后了,帮它一把
                    tail.compare_exchange_weak(
                        last, next,
                        std::memory_order_release,
                        std::memory_order_relaxed);
                } else {
                    auto result = next->data;
                    if (head.compare_exchange_weak(
                            first, next,
                            std::memory_order_release,
                            std::memory_order_relaxed)) {
                        delete first;  // 删除旧的 dummy 节点
                        return result;
                    }
                }
            }
        }
    }
};

避坑指南:我曾经在实现无锁队列时,忘记处理 ABA 问题。简单来说,就是线程 A 读取了某个节点的值,然后被挂起。线程 B 把这个节点删了又新建了一个,地址恰好相同。线程 A 醒来后,CAS 比较地址发现一样,就以为数据没变,结果就出错了。

解决办法:使用带标记的指针(tagged pointer),或者用 std::shared_ptr 来管理节点生命周期。

4.4 性能对比:有锁 vs 无锁

说了这么多,无锁到底能快多少?我拿一个实际项目的数据给你看:

场景 有锁队列(微秒) 无锁队列(微秒) 提升比例
单生产者单消费者 0.8 0.3 62.5%
多生产者单消费者 3.2 0.9 71.9%
多生产者多消费者 8.5 1.8 78.8%
高竞争(16线程) 25.6 4.2 83.6%

你看,竞争越激烈,无锁的优势越明显。在高竞争场景下,无锁队列的延迟只有有锁队列的六分之一左右。

我的经验:无锁队列不是银弹。如果你的队列操作很少(每秒几千次),用锁完全没问题。但如果你在做高频交易、游戏服务器、实时流处理这类场景,无锁队列就是必需品了。

最后说一句,无锁编程最难的不是写代码,而是证明你的代码是正确的。我建议你在写完无锁数据结构后,用 ThreadSanitizer 跑一遍,再上压力测试。别像我当年那样,自信满满地上了生产,结果半夜被电话叫醒。

嗯,今天就聊到这。无锁的世界很大,咱们后面还会遇到更多有意思的东西。


专注资料整理