转自:https://gpp.tkchu.me/double-buffer.html,chatgpt,https://blog.51cto.com/u_15214399/4914060
1.介绍
定义缓冲类封装了缓冲:一段可改变的状态。 这个缓冲被增量地修改,但我们想要外部的代码将修改视为单一的原子操作。 为了实现这点,类保存了两个缓冲的实例:下一缓冲和当前缓冲。 以下情况都满足时,使用这个模式就很恰当:
- 我们需要维护一些被增量修改的状态。
- 在修改到一半的时候,状态可能会被外部请求。
- 我们想要防止请求状态的外部代码知道内部的工作方式。
- 我们想要读取状态,而且不想等着修改完成。
关键点:
- 在状态被修改后,双缓冲需要一个swap步骤。 交换缓冲区的指针或者引用,速度快。 不管缓冲区有多大,交换都只需赋值一对指针。
- 这个模式的另一个结果是增加了内存的使用。 正如其名,这个模式需要你在内存中一直保留两个状态的拷贝。 在内存受限的设备上,你可能要付出惨痛的代价。 如果你不能接受使用两份内存,你需要使用别的方法保证状态在修改时不会被请求。
双缓冲解决的核心问题是状态有可能在被修改的同时被请求。
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-双缓冲队列
队列一般是读写-写写互斥,同一时刻只能读或者写。不同在同一时刻入队/出队。
双缓冲队列,使用两个队列,使用两个锁来进行读写。但是,对于写/读操作而言,同一时刻,仅仅分别支持一个线程进行操作,即该双写队列的并发度为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;
}