双缓冲学习

转自:https://gpp.tkchu.me/double-buffer.html,chatgpt,https://blog.51cto.com/u_15214399/4914060

1.介绍

定义缓冲类封装了缓冲:一段可改变的状态。 这个缓冲被增量地修改,但我们想要外部的代码将修改视为单一的原子操作。 为了实现这点,类保存了两个缓冲的实例:下一缓冲当前缓冲。 以下情况都满足时,使用这个模式就很恰当:

  • 我们需要维护一些被增量修改的状态。
  • 在修改到一半的时候,状态可能会被外部请求。
  • 我们想要防止请求状态的外部代码知道内部的工作方式。
  • 我们想要读取状态,而且不想等着修改完成。

关键点:

  1. 在状态被修改后,双缓冲需要一个swap步骤。 交换缓冲区的指针或者引用,速度快。 不管缓冲区有多大,交换都只需赋值一对指针。
  2. 这个模式的另一个结果是增加了内存的使用。 正如其名,这个模式需要你在内存中一直保留两个状态的拷贝。 在内存受限的设备上,你可能要付出惨痛的代价。 如果你不能接受使用两份内存,你需要使用别的方法保证状态在修改时不会被请求。

双缓冲解决的核心问题状态有可能在被修改的同时被请求。 

2.例子1-双缓冲写日志

应用程序向磁盘写入日志,引入双缓冲区机制,一个缓冲区存储应用程序端发送的日志,按照时间顺序依次存储;另一个缓冲区负责向低层磁盘发送写文件请求。双缓冲区的奇妙之处就在于,两个缓冲区的交换,是通过交换指针来实现的,非常的高效。

#include <iostream>
#include <list>
#include <string>
#include <mutex>
#include <condition_variable>
#include <thread>
#include <chrono>


class Logger {
public:
    Logger() : isSyncRunning(false) {}

    // 应用程序写日志,写入第一个缓冲区
    void log(const std::string& content) {
        {
            std::lock_guard<std::mutex> lock(mutex_);
            currentBuffer.push_back(content);  // 将log写入内存缓冲中
        }
        // 将缓冲区中的内容刷到磁盘
        logSync();
    }

private:
    // 第二缓冲区,向磁盘写日志,并在写入后交换缓冲区指针
    void logSync() {
        std::unique_lock<std::mutex> lock(mutex_);
        
        // 当前在刷内存缓冲到磁盘中去
        if (isSyncRunning) {
            // 判断是否第二个缓冲区还在刷
            condVar_.wait_for(lock, std::chrono::milliseconds(2000), [this] { return !isSyncRunning; });
        }

        // 交换缓冲区指针
        setReadyToSync();

        // 设置当前正在同步到磁盘的标志位
        isSyncRunning = true;

        lock.unlock();

        // 刷磁盘, 性能最低,不能加锁
        flushBuffer();

        lock.lock();
        // 同步完磁盘之后,将标志位复位
        isSyncRunning = false;
        // 唤醒其他等待刷磁盘的线程
        condVar_.notify_all();
    }

    // 模拟磁盘刷入操作
    void flushBuffer() {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));  // 模拟写入磁盘的延迟
        std::cout << "Flushing to disk: ";
        for (const auto& log : syncBuffer) {
            std::cout << log << " ";
        }
        std::cout << std::endl;

        syncBuffer.clear();  // 清空第二缓冲区
    }

    void setReadyToSync() {
        std::swap(currentBuffer, syncBuffer);  // 交换缓冲区指针
    }

private:
    std::list<std::string> currentBuffer;  // 缓冲区1: 负责接收应用程序发来的日志
    std::list<std::string> syncBuffer;     // 缓冲区2: 负责将数据同步到磁盘

    bool isSyncRunning;  // 标志是否正在同步

    std::mutex mutex_;  // 互斥锁,保护临界区
    std::condition_variable condVar_;  // 条件变量用于等待和通知
};

int main() {
    Logger logger;

    // 创建多个线程同时写入日志
    std::thread t1([&] { for (int i = 0; i < 5; ++i) logger.log("Log1"); });
    std::thread t2([&] { for (int i = 0; i < 5; ++i) logger.log("Log2"); });
    
    t1.join();
    t2.join();

    cout<<endl;
    return 0;
}

// 运行结果
// Flushing to disk: Log1 
// Flushing to disk: Log2 Log1 
// Flushing to disk: Log1 
// Flushing to disk: Log1 
// Flushing to disk: Log1 
// Flushing to disk: 
// Flushing to disk: Log2 
// Flushing to disk: Log2 
// Flushing to disk: Log2 
// Flushing to disk: Log2 
两个缓冲区各自处理,互不干扰

两个缓冲区很好的解决了应用程序的“快速、多线程”与IO操作的“缓慢,单线程”的矛盾。应该说,引入双缓冲区是一个显而易见的方式。

3.例子2-双缓冲队列

https://maxwell.gitbook.io/way-to-architect/xi-tong-she-ji/sheng-chan-zhe-xiao-fei-zhe-mo-xing/huan-chong-dui-lie

队列一般是读写-写写互斥,同一时刻只能读或者写。不同在同一时刻入队/出队。

双缓冲队列,使用两个队列,使用两个锁来进行读写。但是,对于写/读操作而言,同一时刻,仅仅分别支持一个线程进行操作,即该双写队列的并发度为2。可以再次使用分段锁。

template <typename T>
class DoubleBufferQueue {
public:
    DoubleBufferQueue() {}

    // 添加元素到队尾
    bool offer(const T& e) {
        std::lock_guard<std::mutex> lock(writeMutex);
        writeQueue.push_back(e);
        return true;
    }

    // 移除并返回队头元素,当读队列为空时,交换队列
    T poll() {
        std::lock_guard<std::mutex> lock(readMutex);
        if (readQueue.empty()) {
            swapNoLock();
        }

        if (readQueue.empty()) {
            throw std::out_of_range("Queue is empty!");
        }

        T front = readQueue.front();
        readQueue.pop_front();
        return front;
    }

    // 返回队头元素(不移除)
    T peek() {
        std::lock_guard<std::mutex> lock(readMutex);
        if (readQueue.empty()) {
            swapNoLock();
        }

        if (readQueue.empty()) {
            throw std::out_of_range("Queue is empty!");
        }

        return readQueue.front();
    }

    // 批量增加元素到队尾
    void addAll(const std::list<T>& elements) {
        std::lock_guard<std::mutex> lock(writeMutex);
        writeQueue.insert(writeQueue.end(), elements.begin(), elements.end());
    }

    // 获取队列总大小
    int size() {
        std::lock_guard<std::mutex> readLock(readMutex);
        std::lock_guard<std::mutex> writeLock(writeMutex);
        return readQueue.size() + writeQueue.size();
    }

private:
    // 读队列和写队列交换
    void swapNoLock() {
        if (!writeQueue.empty()) {
            readQueue.swap(writeQueue);
        }
    }

    std::list<T> readQueue;  // 读队列,出队的时候,从该队列取出元素
    std::list<T> writeQueue; // 写队列,入队的时候,从该队列放入元素

    std::mutex readMutex;    // 保护读队列
    std::mutex writeMutex;   // 保护写队列
};

int main() {

    DoubleBufferQueue<int> queue;
    queue.offer(1);
    queue.offer(2);
    queue.offer(3);

    std::cout << "Peek: " << queue.peek() << std::endl;  // Peek: 1
    std::cout << "Poll: " << queue.poll() << std::endl;  // Poll: 1
    std::cout << "Poll: " << queue.poll() << std::endl;  // Poll: 2
    std::cout << "Poll: " << queue.poll() << std::endl;  // Poll: 3
    cout<<endl;
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值