双buffer切换与代码实现

30 篇文章 0 订阅
文章探讨了在并发环境中处理读写数据时如何保证数据一致性的问题,提出了使用双缓冲方案,通过空间换时间的方式,提高读取效率,但对写入时延和特定场景下的性能影响也进行了分析。
摘要由CSDN通过智能技术生成

概述

在很多场景需要并发的去读写数据,如下图所示:

考虑到数据写入的顺序性,通常只会有一个线程写入,读数据是可以多线程的。

由于对于Data的一次写入不是原子操作,一个常用通常的方式就是在写的时候加写锁,读的时候加读锁。这在同一个线程每次读数据没有依赖时是可行的,否则还是可能出现问题。如在一次数据处理中,先通过用户“姓名”找到Data中对应的id,再通过id去Data中查找用户其它信息。在这两步之间,写线程可能已经把该用户从Data中删除了,这时就会出现异常。如果希望在一次数据处理中保持数据视图的一致,也就是通常说的“事务性”,需要添加一些措施。

最简单的方式,将上面的读写锁改为普通互斥锁,问题就解决了。但如果写数据的时间开销较大,那么所有的读线程都需要等待数据写完后才能继续工作,无法满足对读响应实时性要求很高的场景(也是笔者目前遇到的情形)。

思路

我是这么去思考的,如果是写数据的时间开销较大,那么是否有办法缩短呢(以至于可以忽略不计的程度)。如果写的数据还可以细分,我们是可以考虑这种方案的。而本文介绍的双buffer方案则是一种空间换时间的算法,它同时存储了两份数据,读的时候去主数据中查询,而写的时候则写入备份数据中,当完成写入后将主/备数据进行交换即可。

数据读取

step1: 获取读锁,确保此时并发的写线程不会修改主数据,同时多线程读数据不会阻塞;

step2: 获取数据,如果需要保证事务性,可以在多次查询中保留读锁不释放;

step3: 释放读锁;

数据写入

step1: 获取当前的数据版本,明确哪份是主数据,哪份是备份数据

step2: 将备份数据与主数据保持同步,由于此时并未加锁,读线程仍然可以从主数据中查询数据

step3: 将数据写入备份数据中,由于此时并未加锁,读线程仍然可以从主数据中查询数据

step4: 加写锁,读数据不再允许,因为数据要改动了

step5: 更新数据版本,这是一个开销很小的操作

step6: 释放写锁

代码实现

#include <shared_mutex>
#include <memory>
#include <iostream>

struct DoubleBuffering {
    public:
        DoubleBuffering() {
            _mtx = std::make_shared<std::shared_mutex>();
        }
        
        // following two functions used in query condition
        void hold_read_lock() { return _mtx->lock_shared(); }
        void release_read_lock() { return _mtx->unlock_shared(); }

        // following function could be called with or without _mtx locked
        int get_version() { return _version;}

        void exchange() {
            std::unique_lock<std::shared_mutex> lock(*_mtx);
            if(_version == 0) {
                std::cout << std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count() << "\tversion update from 0 to 1: " << std::endl;
                _version = 1;
            }else {
                _version = 0;
                std::cout << std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count() << "\tversion update from 1 to 0: " << std::endl;
            }
        }

    public:
        int _version{0};
        std::shared_ptr<std::shared_mutex> _mtx{nullptr}; 
};

验证代码

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

using namespace std;

// global variable
int number0 = 0;
int number1 = 0;
DoubleBuffering buffer;
mutex mtx;

void read_func() {
    for(int i=0; i!=10; i++) {
        buffer.hold_read_lock();
        int version = buffer.get_version();
        if(version == 0) {
            std::unique_lock<std::mutex> lock(mtx);
            cout << i << "\t0\t" << std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count() << "\t" << number0 << "\t" << number1 << endl;
        }else {
            std::unique_lock<std::mutex> lock(mtx);
            cout << i << "\t1\t" << std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count() << "\t" << number0 << "\t" << number1 << endl;
        }
        // simulate procedure
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        buffer.release_read_lock();
    }
}

void write_func() {
    for(int i=0; i!=10; i++) {
        int version = buffer.get_version();
        if(version == 1) { // version 1 data might be using now, so we can safely sync and update number0 without lock
            // sync
            number0 = number1;
            // write new data
            number0 = number0 + i;
        }else {
            // sync
            number1 = number0;
            // write new data
            number1 = number1 + i;
        }
        buffer.exchange();
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

int main() {
    // start two read threads and one write thread
    thread read1_thread(read_func);
    thread read2_thread(read_func);
    thread write_thread(write_func);
    read1_thread.join();
    read2_thread.join();
    write_thread.join();
    return 0;
}

总结

这种双buffer切换的方式主要适合读高频,写低频,且读对数据响应的实时性要求很高的场景。它也会有“弱点”,可能在以下场景下不再合适:

如果对写入的实时性较高,同时读数据一次事务的时间较长,这样写数据会被阻塞住;

如果读/写入的频率都很高,写操作被阻塞的可能性也很大,造成写入的吞吐量降低。

  • 8
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
下面是一个简单的 C++ 实现 framebuffer 缓冲: ```cpp #include <iostream> #include <vector> class Framebuffer { public: Framebuffer(int width, int height) : width_(width), height_(height) { // 初始化两个缓冲区 back_buffer_.resize(width_ * height_); front_buffer_.resize(width_ * height_); } // 渲染函数,将 front_buffer_ 渲染到屏幕上 void render() { for (int i = 0; i < height_; ++i) { for (int j = 0; j < width_; ++j) { std::cout << front_buffer_[i * width_ + j] << " "; } std::cout << std::endl; } } // 将数据写入 back_buffer_ void write(int x, int y, int value) { back_buffer_[y * width_ + x] = value; } // 切换缓冲区,front_buffer_ 变为 back_buffer_,back_buffer_ 变为 front_buffer_ void swap() { std::swap(back_buffer_, front_buffer_); } private: int width_; int height_; std::vector<int> back_buffer_; // 后缓冲区 std::vector<int> front_buffer_; // 前缓冲区 }; int main() { Framebuffer framebuffer(10, 10); framebuffer.write(1, 1, 1); framebuffer.write(2, 2, 2); framebuffer.write(3, 3, 3); framebuffer.swap(); // 切换缓冲区 framebuffer.render(); // 将 front_buffer_ 渲染到屏幕上 return 0; } ``` 在这个实现中,我们使用了两个 `std::vector` 来存储两个缓冲区,`back_buffer_` 表示后缓冲区,`front_buffer_` 表示前缓冲区。在每次渲染前,我们将 `front_buffer_` 渲染到屏幕上,然后在渲染过程中将数据写入到 `back_buffer_` 中。在需要切换缓冲区时,我们只需要交换 `back_buffer_` 和 `front_buffer_` 即可。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值