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直接挂掉。嗯,从那以后,我再也不敢忽视背压了。
实现背压,常见的有三种方式:
- 阻塞式背压:队列满了,生产者线程直接阻塞等待。实现简单,但容易导致线程死锁。
- 丢弃式背压:队列满了,直接丢弃新来的请求。适合预览场景,丢几帧无所谓。
- 反馈式背压:通过回调或信号量通知生产者“我忙不过来了,你慢点”。最优雅,但实现复杂。
注意:阻塞式背压一定要小心死锁。比如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池”方案,保证让你眼前一亮。