核心在于用循环结构驱动缓冲区角色自动翻转,配合原子状态机控制读写边界,避开锁竞争,让行情数据流“写入不卡、读取不撕裂、切换不丢帧”。

一、循环结构要匹配行情节奏,不能只靠 while(1)
高频行情数据(如 Level2 行情、逐笔委托)通常以微秒级间隔到达,单纯用死循环轮询会浪费 CPU 且无法对齐数据包边界。正确做法是:让循环体与数据就绪信号对齐。
- 在 DMA 或网络接收层,配置硬件/IO 多路复用(epoll/kqueue)触发回调,而非忙等;回调中仅做指针标记+唤醒,不处理业务逻辑
- 主消费循环采用“条件等待 + 批量处理”模式:sleep(0) 或 std::this_thread::yield() 配合原子状态检查,避免空转;一旦检测到 buffer 状态为 待读(如 atomic_state == 2),立即批量 memcpy 整块数据并重置状态
- 循环内不包含 new/malloc、异常抛出、日志打印等非确定性操作——这些必须移出循环体,放到独立线程或异步队列中
二、双缓冲区结构必须固定大小、零拷贝可交换
不能用 std::vector 或 std::string 做底层容器,它们的动态扩容和异常安全性会破坏原子切换前提。
- 推荐方案:两个 std::array<uint8_t, 1024*1024>(1MB),或堆上分配的 unique_ptr<uint8_t[]> + 显式 size 字段;大小选 512KB~2MB,兼顾 L3 缓存命中率与单次系统调用延迟
- 用 std::atomic<int> 管理三态:0=可写、1=切换中、2=待读;禁止用 bool 或指针直接 swap,否则无法捕获“写了一半就被翻走”的竞态
- 切换动作必须带内存序:atomic_thread_fence(memory_order_release) 在写完后插入,atomic_thread_fence(memory_order_acquire) 在读前插入,防止编译器/CPU 乱序导致读到未提交数据
三、无锁读取的关键不是“不加锁”,而是“不阻塞+不撕裂”
行情系统最怕两种错误:一是消费者被锁住导致堆积,二是读到半条行情(比如只读到 OrderID 没读到 Price)。
- 生产者写入前检查状态是否为 0;若为 1(正在切换),短时 yield 后 fallback 到线程局部缓存(thread_local char[4096]),攒满再触发一次翻转
- 消费者读取整块 buffer 时,需按协议头校验完整消息边界(例如 FIX TagValue、Thrift Struct 或自定义 length-prefix),跳过残帧,不假设 buffer 内全是有效数据
- 绝不允许跨 buffer 读一条消息——每条行情必须完全落在一个 buffer 内;设计协议时预留 headroom,写入端确保单条消息 ≤ buffer_size × 0.8
四、实战中容易被忽略的三个细节
很多团队实现了双缓冲结构却仍出现丢帧或延迟抖动,问题往往不在算法,而在配套机制。
- CPU 绑核:将生产者线程绑定到物理核心 A,消费者绑定到核心 B,避免 L3 缓存伪共享;DMA 内存页用 mlock() 锁住,防止缺页中断打断实时性
- 时间戳对齐:每个 buffer 切换时刻记录 clock_gettime(CLOCK_MONOTONIC),后续用于判断是否超时未消费(>10ms 触发告警,>100ms 强制丢弃旧 buffer)
- 崩溃防护:buffer 中不存裸指针或虚函数表地址;落盘日志用 writev() 批量刷,fsync() 仅在进程退出前或 buffer 满 95% 时触发,不随每次翻转执行

















