关键词:原子指针、双buffer缓冲、无锁、C++、延迟敏感
为什么需要双缓冲?单线程不行吗?
- 单缓冲的问题 :
如果只有一个缓冲区,读取线程(生产者)在填充数据时,渲染线程(消费者)必须等待,反之亦然。此时两者串行执行,总耗时 = 读取耗时 + 渲染耗时,无法利用现代多核 CPU 的并行能力。而且,如果渲染耗时远大于读取耗时,会导致读取线程频繁阻塞,丢帧严重。 - 双缓冲的收益 :
双缓冲允许多少程度上"流水线"并行:读取线程写入 写缓冲 (g_read_buffer指向),渲染线程从 读缓冲 (g_render_buffer指向)取出数据进行渲染。只要两个线程操作的不是同一个缓冲,它们就可以并发执行,读写的索引通过原子变量快速"交换",让下一次读写指向另一个缓冲。这样,读取和渲染在时间上重叠,有效降低了整体延迟,提高了吞吐量。
原子变量如何保证线程安全
代码中使用了 std::atomic<int> 来存储缓冲索引,并用 exchange 交换两个索引。
-
原子操作的作用 :
std::atomic保证了读写操作的 原子性 ,即读取或修改变量时不会被打断,不会出现"读到一半被另一个线程改掉"的情况。exchange是读-改-写操作(RMW),它自动完成"先读取旧值,再写入新值,返回旧值"的整个过程,不会被中断。 -
无锁同步的关键 :
代码通过两个原子变量实现一种 双缓冲槽位交换 算法:iniint old_render = g_render_buffer.exchange(write_idx); g_read_buffer.store(old_render);
读取线程先拿到当前写缓冲 write_idx,填充完成后,它把 write_idx 交给渲染线程(存到 g_render_buffer),同时把渲染线程原来持有的索引(old_render)收回来作为下一次的写缓冲。这个交换是 无锁 的,不需要互斥锁,性能很高。
- 数据竞争与内存序 :
虽然原子操作保证了索引变量本身的完整性,但 缓冲区内容(images数组)的修改与索引的发布之间必须建立 happens-before 关系 。否则,渲染线程可能看到新的索引,却读到尚未完全写入的图像数据(即部分更新的帧)。
在默认的std::memory_order_seq_cst下,store和exchange会提供顺序一致性,保证所有之前的写操作(图像填充)在索引交换之前"提交",所以渲染线程看到新索引时,一定也能看到完整的新帧。这是安全的。
如果为了性能改用宽松内存序(如memory_order_release/acquire),则需要额外小心。这里使用默认顺序,简单且安全
5. 除了双缓冲,还有哪些同步方式?各有什么优缺点?
比较双缓冲(double buffering)、互斥锁+条件变量、无锁队列(lock-free queue)在这类生产者-消费者场景下的优劣。 问题:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 双缓冲 + 原子交换 | 极低的同步开销,无锁,实现简单,适合固定大小缓冲区;能有效减少延迟。 | 只能容纳两个缓冲,若生产或消费速度波动较大,可能丢帧或空闲等待;不支持动态扩展。 |
| 互斥锁 + 条件变量 | 通用性强,可支持多缓冲(如使用 std::queue);生产者/消费者可以灵活增减。 |
有锁竞争和上下文切换开销;条件变量可能导致惊群问题;需要小心死锁。 |
无锁队列(如 boost::lockfree::queue) |
高性能,无阻塞,支持多生产者多消费者;可动态扩展。 | 实现复杂;内存管理困难(需要自行回收节点);ABA 问题(但可用带标签指针解决)。 |
适用场景选择:
- 若生产者和消费者速度相对稳定,且对延迟要求高(如实时视频),双缓冲 是性价比最高的选择。
- 若速度波动大,且需要平滑处理(如网络流),使用 环形缓冲区(ring buffer) 或多缓冲 + 条件变量更合适。
- 若有多生产者和多消费者,无锁队列 是主流选择
示例代码
shell
#include <iostream>
#include <thread>
#include <chrono>
#include <atomic>
#include <vector>
#include <signal.h>
#include <opencv2/opencv.hpp>
// ---------- 双缓冲数据结构 ----------
struct FrameData {
std::vector<cv::Mat> images; // 每路相机一帧
int64_t frame_idx = 0;
};
// ---------- 全局状态 ----------
static std::atomic<bool> g_quit{false};
static std::atomic<bool> g_read_thread_running{false};
static std::atomic<bool> g_loop_reset{false}; // 标记视频循环重置
static std::atomic<bool> g_frame_ready{false}; // 完整帧写入完成
// 双缓冲
static FrameData g_frame_buffers[2];
static std::atomic<int> g_read_buffer{0}; // 读取线程当前写入的缓冲索引
static std::atomic<int> g_render_buffer{1}; // 渲染线程当前读取的缓冲索引
// ---------- 信号处理 ----------
void signal_handler(int signum) {
if (signum == SIGINT || signum == SIGTERM) {
g_quit = true;
std::cout << "收到退出信号,准备释放资源..." << std::endl;
}
}
// ---------- 视频读取线程 ----------
static void videoReadThread(int cam_cnt, std::vector<cv::VideoCapture>& captures) {
while (g_read_thread_running) {
// 选择当前可写的缓冲索引
int write_idx = g_read_buffer.load();
// 读取所有相机帧
bool all_read = true;
for (int i = 0; i < cam_cnt; ++i) {
captures[i] >> g_frame_buffers[write_idx].images[i];
if (g_frame_buffers[write_idx].images[i].empty()) {
all_read = false;
break;
}
}
// 若任意一路视频结束,则重置所有视频到开头(循环播放)
if (!all_read) {
for (int i = 0; i < cam_cnt; ++i) {
captures[i].set(cv::CAP_PROP_POS_FRAMES, 0);
}
g_loop_reset = true; // 通知主线程重置状态
continue;
}
g_loop_reset = false;
g_frame_buffers[write_idx].frame_idx++;
// 原子交换缓冲:让渲染线程使用刚写好的缓冲
int old_render = g_render_buffer.exchange(write_idx);
g_read_buffer.store(old_render);
// 标记完整帧就绪
g_frame_ready.store(true);
}
}
// ========== 主程序 ==========
int main(int argc, char** argv) {
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
// -------------------- 配置 --------------------
const int cam_cnt = 2; // 假设2路视频
std::vector<std::string> video_paths = {
"/home/data1.mp4",
"/home/data2.mp4",
};
std::vector<cv::VideoCapture> captures(cam_cnt);
for (int i = 0; i < cam_cnt; ++i) {
if (!captures[i].open(video_paths[i])) {
std::cerr << "无法打开视频: " << video_paths[i] << std::endl;
return -1;
}
}
// 初始化双缓冲
for (int b = 0; b < 2; ++b) {
g_frame_buffers[b].images.resize(cam_cnt);
for (int i = 0; i < cam_cnt; ++i) {
g_frame_buffers[b].images[i] = cv::Mat();
}
g_frame_buffers[b].frame_idx = 0;
}
g_read_buffer.store(0);
g_render_buffer.store(1);
g_frame_ready.store(false);
g_loop_reset.store(false);
// -------------------- 启动读取线程 --------------------
g_read_thread_running = true;
std::thread reader_thread(videoReadThread, cam_cnt, std::ref(captures));
// 等待第一帧就绪
while (!g_frame_ready.load() && g_read_thread_running) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
// -------------------- 主渲染循环 --------------------
int frame_count = 0;
const int max_frames = -1; // 演示运行300帧后退出(可改为-1无限循环)
cv::Mat display_img;
while (true) {
if (g_quit) break;
if (max_frames > 0 && frame_count >= max_frames) break;
// 若视频循环重置,短暂等待线程完成重置
if (g_loop_reset.load()) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
continue;
}
// 等待完整帧就绪
while (!g_frame_ready.load()) {
if (g_quit) break;
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
g_frame_ready.store(false);
// 无锁获取当前渲染缓冲索引
int render_idx = g_render_buffer.load();
const auto& frame = g_frame_buffers[render_idx];
// ---------- 此处为"渲染"处理 ----------
// 将第一路图像缩放并显示(演示),实际可换成任何处理
if (!frame.images[0].empty()) {
cv::resize(frame.images[0], display_img, cv::Size(640, 480));
// 可叠加帧号
cv::putText(display_img, "Frame: " + std::to_string(frame.frame_idx),
cv::Point(30, 50), cv::FONT_HERSHEY_SIMPLEX, 1.0,
cv::Scalar(0, 255, 0), 2);
}
// 显示
cv::imshow("双缓冲演示 (流水线并行)", display_img);
int key = cv::waitKey(10); // 10ms延时,同时处理GUI事件
if (key == 'q' || key == 27) { // q或ESC退出
g_quit = true;
break;
}
frame_count++;
}
// -------------------- 清理 --------------------
g_read_thread_running = false;
if (reader_thread.joinable()) {
reader_thread.join();
}
for (auto& cap : captures) {
cap.release();
}
cv::destroyAllWindows();
std::cout << "程序正常退出" << std::endl;
return 0;
}