返回顶部

C++ DoubleBuffer 双缓冲技术

一、背景:要解决什么问题?

在高并发服务中,经常会遇到这样的需求

  • 配置 / 策略 / 规则 需要动态更新

  • 读请求 非常频繁

  • 更新 相对较少

  • 不能因为更新 阻塞或影响读请求

核心诉求:读要极快,最好无锁;写可以慢一点,但要安全。

1、最基础写法

std::string current_notice;

std::string getNotice() {
    return current_notice;
}

void updateNotice(const std::string& n) {
    current_notice = n;
}

问题:在多线程下:一个线程在读 current_notice,另一个线程正在改 current_notice,这就会造成

  • 数据竞争
  • 可能读到一半
  • 未定义行为

2、加锁写法

std::mutex m;
std::string current_notice;

std::string getNotice() {
    std::lock_guard<std::mutex> lk(m);
    return current_notice;
}

void updateNotice(const std::string& n) {
    std::lock_guard<std::mutex> lk(m);
    current_notice = n;
}

问题:读请求非常多,每次都要抢锁,高并发下 性能下降明显。

3、读写锁优化

std::shared_mutex rw_lock;
std::string current_notice;

std::string getNotice() {
    std::shared_lock<std::shared_mutex> lk(rw_lock);
    return current_notice;
}

void updateNotice(const std::string& n) {
    std::unique_lock<std::shared_mutex> lk(rw_lock);
    current_notice = n;
}

改进:读锁可以并发,但仍然有锁开销。在极高并发场景下(如每秒百万级读请求),锁竞争依然会成为瓶颈。

二、DoubleBuffer 是什么?

DoubleBuffer(双缓冲) 的核心思想是:始终维护两份数据

  • 一份给读线程用(active)

  • 一份给写线程改(inactive)

当写完后,将写线程数据切换给读线程。
基本流程:

初始状态:
  buffer[0] ← 读线程在用(active)
  buffer[1] ← 写线程可以改(inactive)

更新流程:
  1. 写线程修改 buffer[1]
  2. 切换指针:active = buffer[1]
  3. 下次更新时改 buffer[0]

问题:谁负责"等读线程看完"再回收旧数据?

假设:

  • 读线程 A 正在读 buffer[0]
  • 写线程切换到 buffer[1]
  • 读线程 B 开始读 buffer[1]
  • 写线程想要修改 buffer[0]...

读线程 A 还没读完呢!如果直接改 buffer[0],就会出现数据竞争。

三、shared_ptr:自动“等所有读线程看完后再撤”

shared_ptr引用计数的方式可以确认当前是否有读操作。

  • 没有读操作的时候引用计数清零自动析构,
  • 再次读的时候直接从写操作拷贝一份来读

实现原理:

#include <memory>
#include <atomic>
#include <mutex>

template<typename T>
class DoubleBuffer {
public:
    DoubleBuffer() {
        // 初始化两个buffer
        buffers_[0] = std::make_shared<T>();
        buffers_[1] = std::make_shared<T>();
        index_.store(0);
    }

    // 读操作:无锁,只有原子操作
    std::shared_ptr<T> read() {
        return std::atomic_load(&buffers_[index_.load()]);
    }

    // 写操作:需要加锁保证写写互斥
    template<typename Func>
    void modify(Func&& func) {
        std::lock_guard<std::mutex> lock(write_mutex_);
        
        // 获取当前inactive的buffer索引
        int current_idx = index_.load();
        int next_idx = 1 - current_idx;
        
        // 复制当前数据到inactive buffer
        auto new_data = std::make_shared<T>(*buffers_[current_idx]);
        
        // 修改新数据
        func(*new_data);
        
        // 更新inactive buffer
        buffers_[next_idx] = new_data;
        
        // 原子切换索引
        index_.store(next_idx);
        
        // 旧的buffer会在所有shared_ptr释放后自动析构
    }

private:
    std::shared_ptr<T> buffers_[2];
    std::atomic<int> index_{0};
    std::mutex write_mutex_;  // 保护写操作
};

为什么 shared_ptr 能解决问题?

1.读线程获取 shared_ptr

auto data = double_buffer.read();  // 引用计数 +1
   // 使用 data...

2.写线程切换 buffer

// 即使切换了,旧的 buffer 因为还被读线程持有
   // 引用计数不为 0,不会被析构
   index_.store(next_idx);

3.读线程用完

}  // data 离开作用域,引用计数 -1
      // 如果降为 0,自动析构

四、完整使用示例

示例:热配置更新

#include <iostream>
#include <thread>
#include <chrono>

struct Config {
    std::string server_url;
    int timeout_ms;
    bool debug_mode;
};

int main() {
    DoubleBuffer<Config> config_buffer;
    
    // 初始化配置
    config_buffer.modify([](Config& cfg) {
        cfg.server_url = "http://api.example.com";
        cfg.timeout_ms = 3000;
        cfg.debug_mode = false;
    });
    
    // 读线程:高频读取配置
    auto reader = [&]() {
        for (int i = 0; i < 1000000; ++i) {
            auto cfg = config_buffer.read();  // 无锁读取
            // 使用配置...
            if (i % 100000 == 0) {
                std::cout << "Read: " << cfg->server_url 
                          << ", timeout=" << cfg->timeout_ms << "\n";
            }
        }
    };
    
    // 写线程:定期更新配置
    auto writer = [&]() {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        config_buffer.modify([](Config& cfg) {
            cfg.server_url = "http://new-api.example.com";
            cfg.timeout_ms = 5000;
            cfg.debug_mode = true;
        });
        std::cout << "Config updated!\n";
    };
    
    std::thread t1(reader);
    std::thread t2(reader);
    std::thread t3(writer);
    
    t1.join();
    t2.join();
    t3.join();
    
    return 0;
}

五、性能对比

测试场景:100万次读,1次写

方案 读操作耗时 写操作耗时
互斥锁(mutex) ~500ms ~10μs
读写锁(shared_mutex) ~200ms ~20μs
DoubleBuffer ~50ms ~100μs

结论:DoubleBuffer 在读多写少的场景下,读性能提升 4-10 倍。

六、DoubleBuffer 的优缺点

优点

  • 读操作几乎无锁:只有一次原子加载 + shared_ptr拷贝
  • 读写不互斥:读线程不会被写线程阻塞
  • 自动内存管理:shared_ptr 自动回收旧数据
  • 线程安全:天然避免数据竞争

缺点

  • 内存开销:始终维护两份数据
  • 写操作需要拷贝:每次写都要复制整个对象
  • 不适合大对象:如果数据结构很大(如GB级),拷贝开销很高
  • 写写互斥:多个写线程之间仍需加锁
posted @ 2025-12-25 11:07  十方央丶  阅读(24)  评论(0)    收藏  举报