【C++】学习:双缓冲视频流水线深解析

关键词:原子指针、双buffer缓冲、无锁、C++、延迟敏感

为什么需要双缓冲?单线程不行吗?

  • 单缓冲的问题
    如果只有一个缓冲区,读取线程(生产者)在填充数据时,渲染线程(消费者)必须等待,反之亦然。此时两者串行执行,总耗时 = 读取耗时 + 渲染耗时,无法利用现代多核 CPU 的并行能力。而且,如果渲染耗时远大于读取耗时,会导致读取线程频繁阻塞,丢帧严重。
  • 双缓冲的收益
    双缓冲允许多少程度上"流水线"并行:读取线程写入 写缓冲g_read_buffer 指向),渲染线程从 读缓冲g_render_buffer 指向)取出数据进行渲染。只要两个线程操作的不是同一个缓冲,它们就可以并发执行,读写的索引通过原子变量快速"交换",让下一次读写指向另一个缓冲。这样,读取和渲染在时间上重叠,有效降低了整体延迟,提高了吞吐量

原子变量如何保证线程安全

  代码中使用了 std::atomic<int> 来存储缓冲索引,并用 exchange 交换两个索引。

  • 原子操作的作用
    std::atomic 保证了读写操作的 原子性 ,即读取或修改变量时不会被打断,不会出现"读到一半被另一个线程改掉"的情况。exchange 是读-改-写操作(RMW),它自动完成"先读取旧值,再写入新值,返回旧值"的整个过程,不会被中断。

  • 无锁同步的关键
    代码通过两个原子变量实现一种 双缓冲槽位交换 算法:

    ini 复制代码
    int 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 下,storeexchange 会提供顺序一致性,保证所有之前的写操作(图像填充)在索引交换之前"提交",所以渲染线程看到新索引时,一定也能看到完整的新帧。这是安全的。
    如果为了性能改用宽松内存序(如 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;
}
相关推荐
青少儿编程课堂2 小时前
图形化编程实战:智能交通灯调度台,一个作品讲透循环、条件与广播
c++·python·算法·bfs·信息学竞赛
赵民勇2 小时前
C++无锁编程详解
c++
醉颜凉2 小时前
C++ 中野指针与悬挂指针的区别:从成因到防范,彻底讲清两种危险指针
c++
Demon--hx2 小时前
设计不能被继承的类
开发语言·c++
兔兔兔兔12 小时前
记录C++ 13
开发语言·c++·算法
开心大爆炸3 小时前
ubuntu20.4 安装ros1 noetic版本流程
c++
欧特克_Glodon5 小时前
OpenCV计算机视觉开发入门与实践<二十四>:几何变换之平移、缩放、旋转
c++·人工智能·opencv·计算机视觉
鱼很腾apoc5 小时前
【Linux】第13期 详解应用层协议HTTP+Cookie/Session+HTTPS
linux·服务器·c++·学习·http·https
charlie1145141916 小时前
deque、list 与 forward_list:vector 之外的三个选择
开发语言·数据结构·c++·list·开源项目