4、Buffer队列管理:请求队列与完成队列、队列深度控制、背压机制实现

好,咱们接着聊Buffer队列管理。说实话,这部分是Camera HAL里最容易出问题的地方之一。我见过太多团队,算法写得漂亮,硬件也调得不错,结果就在队列管理上栽了跟头。

队列管理说白了就三件事:怎么把请求送进去,怎么把结果拿回来,以及怎么控制中间这个“管道”不堵死。咱们一个一个说。

4.1 请求队列与完成队列:生产者和消费者的博弈

先看个最简单的模型。上层Framework下发Capture Request,HAL收到后开始处理,处理完把结果塞回给Framework。这中间就需要两个队列:

  • 请求队列(Request Queue):存放Framework发来的待处理请求
  • 完成队列(Complete Queue):存放HAL处理完的待返回结果

嗯,听起来很简单对吧?但实际做起来,坑可不少。

核心原则:请求队列和完成队列必须解耦。千万别让HAL的处理线程直接往Framework里塞数据,否则一个回调卡住,整个Pipeline就挂了。

我个人习惯的做法是:每个队列配一个独立的线程。请求队列由“请求分发线程”管理,完成队列由“结果上报线程”管理。两个线程各干各的,互不干扰。

// 伪代码示例:请求队列与完成队列的分离管理
class CameraPipeline {
    std::queue<CaptureRequest> requestQueue;
    std::queue<CaptureResult> completeQueue;
    
    std::thread requestDispatcher;  // 请求分发线程
    std::thread resultReporter;     // 结果上报线程
    
    void start() {
        requestDispatcher = std::thread([this]() { dispatchLoop(); });
        resultReporter = std::thread([this]() { reportLoop(); });
    }
    
    void dispatchLoop() {
        while (running) {
            CaptureRequest req;
            if (requestQueue.pop(req)) {
                processRequest(req);  // 交给硬件处理
            }
        }
    }
    
    void reportLoop() {
        while (running) {
            CaptureResult res;
            if (completeQueue.pop(res)) {
                notifyFramework(res);  // 回调给上层
            }
        }
    }
};

你看,这样设计的好处是:即使Framework的回调函数执行得很慢,也不会阻塞HAL内部的请求处理。我在项目中遇到过,某个OEM的Framework层回调里做了大量图像后处理,导致整个拍照流程卡死。后来改成双队列+独立线程,问题就解决了。

4.2 队列深度控制:到底该放多少个请求?

队列深度,说白了就是队列里最多能同时放多少个请求。这个值设大了,内存占用高;设小了,吞吐量上不去。

那到底设多少合适?我给大家一个参考值:

场景 推荐队列深度 说明
预览(Preview) 2~4 预览延迟敏感,深度不宜过大
拍照(Still Capture) 1~2 拍照质量优先,深度小可减少内存压力
视频录制(Video) 4~8 需要平衡延迟和吞吐量
高速连拍(Burst) 8~16 追求高帧率,深度可适当加大

我的经验:别把队列深度设成固定值。我建议做成动态可调的,根据当前场景和系统负载自动调整。比如预览时用2,检测到用户按下快门时临时增大到4,拍完再降回来。

你想想看,如果队列深度设得太大,比如预览时放了16个请求,那用户转动手机时,你看到的画面可能还是半秒前的——这就是延迟。反过来,如果深度设成1,那硬件稍微慢一点,队列就空了,帧率直接掉一半。

4.3 背压机制实现:别让生产者跑得太快

背压(Backpressure),这个词听起来高大上,其实意思很简单:当消费者处理不过来时,让生产者慢下来

在Camera HAL里,背压机制尤其重要。为什么?因为Framework下发请求的速度,和HAL处理请求的速度,往往是不匹配的。Framework可能一口气下发几十个请求,但HAL的ISP处理能力有限,处理不完怎么办?

我曾经见过一个项目,没有做背压控制,结果Framework疯狂下发请求,HAL的请求队列越积越多,最后内存爆了,整个Camera Service直接挂掉。嗯,从那以后,我再也不敢忽视背压了。

实现背压,常见的有三种方式:

  1. 阻塞式背压:队列满了,生产者线程直接阻塞等待。实现简单,但容易导致线程死锁。
  2. 丢弃式背压:队列满了,直接丢弃新来的请求。适合预览场景,丢几帧无所谓。
  3. 反馈式背压:通过回调或信号量通知生产者“我忙不过来了,你慢点”。最优雅,但实现复杂。

注意:阻塞式背压一定要小心死锁。比如Framework线程在等待HAL处理完,而HAL线程又在等待Framework释放Buffer,这就形成了循环等待。我建议用带超时的阻塞,比如std::condition_variable的wait_for()。

我个人比较推荐反馈式背压。具体做法是:HAL维护一个“可用Buffer计数”,Framework每下发一个请求,计数减1;HAL每处理完一个请求,计数加1。当计数降到0时,HAL通过回调通知Framework“暂停下发”。

// 反馈式背压的简化实现
class BackpressureController {
    std::atomic<int> availableBuffers_{MAX_BUFFERS};
    std::function<void()> onPauseCallback_;
    std::function<void()> onResumeCallback_;
    
public:
    bool tryAcquire() {
        if (availableBuffers_.load() <= 0) {
            onPauseCallback_();  // 通知Framework暂停
            return false;
        }
        availableBuffers_.fetch_sub(1);
        return true;
    }
    
    void release() {
        int prev = availableBuffers_.fetch_add(1);
        if (prev == 0) {
            onResumeCallback_();  // 通知Framework恢复
        }
    }
};

你看,这个实现里有个关键点:只在状态切换时通知。比如从“满”变“不满”时才通知恢复,而不是每次release都通知。这样可以避免频繁的回调开销。

4.4 实战中的避坑指南

最后,我总结几个实际项目中容易踩的坑:

  • 坑一:队列操作不加锁。多线程环境下,队列的push和pop必须加锁。别用std::queue裸奔,至少包一层std::mutex。
  • 坑二:背压阈值设得太激进。比如队列深度是8,你设成队列到6就开始背压。结果系统稍微波动一下,帧率就掉得厉害。建议留20%~30%的缓冲空间。
  • 坑三:忘记处理队列清空。当Camera关闭时,队列里可能还有未处理的请求。一定要在stop()函数里清空队列,并释放所有Buffer,否则会有内存泄漏。
  • 坑四:背压和Buffer复用混在一起。背压控制的是请求数量,Buffer复用控制的是内存分配。两者要分开管理,别混为一谈。

一句话总结:队列管理做得好,Camera HAL就成功了一半。请求队列和完成队列要分离,队列深度要动态调整,背压机制要优雅实现。这三件事做好了,你的Pipeline就能跑得又快又稳。

好了,这一章就到这里。下一章咱们聊聊Buffer的复用策略,这可是提升性能的关键。到时候我会分享一个我在某旗舰机型上用的“三级Buffer池”方案,保证让你眼前一亮。