基于 C 语言的线程安全的图像缓冲队列
线程安全的图像缓冲队列
在这篇博客中,笔者将探讨一个最近实现的基于 C 语言的线程安全图像缓冲循环队列,重点分析其内存管理的细节和注意事项。该队列用于在图像处理后入队,供后续 AI 推理时出队使用,特别适用于实时视频处理或 AI 推理流水线。以下笔者将聚焦内存管理的实现细节、潜在风险及优化建议。
1. 介绍
该图像缓冲队列是一个固定大小的线程安全循环队列,基于 OpenCV 的 Mat
结构存储图像数据,采用生产者-消费者模型。内存管理是笔者重点考虑的细节,需要确保队列在高负载场景下的稳定性和高效性:
- 生产者:预处理图像(如调整大小、归一化)并入队。
- 消费者:出队图像并送入 AI 模型进行推理。
队列的关键特性包括:
- 使用 POSIX 线程(
pthread
)的互斥锁和条件变量实现线程安全。 - 通过循环缓冲区优化内存使用。
- 支持图像元数据(名称、路径)和 OpenCV 图像数据的管理。
2. 关键代码
2.1 数据结构
c_mat_t
:OpenCV Mat
的封装,存储图像数据及其元数据。
typedef struct c_mat {
unsigned char* mat_data;
int mat_data_size;
int mat_width;
int mat_height;
int mat_channels;
int mat_type;
} c_mat_t;
- 内存管理要点:
mat_data
是一个指针,指向动态分配的实际图像数据。调用者需负责分配和释放内存,队列本身不管理mat_data
的生命周期。
ImageInfo
是存储图像元数据和 c_mat_t
指针; ImageQueue
是循环队列结构,包含 ImageInfo 数组和同步原语:
typedef struct {
char name[MAX_NAME_LEN];
char path[MAX_NAME_LEN];
c_mat_t *img;
} ImageInfo;
typedef struct {
ImageInfo items[MAX_QUEUE_SIZE];
int front;
int rear;
int size;
pthread_mutex_t mutex;
pthread_cond_t not_full;
pthread_cond_t not_empty;
} ImageQueue;
- 内存管理要点:
items
是一个固定大小的数组(MAX_QUEUE_SIZE = 10
),在栈或全局内存中分配,无需动态分配。mutex
和条件变量由 POSIX 线程库管理。
2.2 入队出队核心函数
从该队列设计上讲:调用者手动管理每一张图片的内存创建与释放,因此初始化队列和销毁队列的设计比较简单且常规,不再赘述。
入队图像:
int enqueue(ImageQueue *queue, const char *name, const char *path, const c_mat_t *img) {
pthread_mutex_lock(&queue->mutex);
while (queue->size == MAX_QUEUE_SIZE) {
pthread_cond_wait(&queue->not_full, &queue->mutex);
}
queue->rear = (queue->rear + 1) % MAX_QUEUE_SIZE;
strncpy(queue->items[queue->rear].name, name, MAX_NAME_LEN - 1);
strncpy(queue->items[queue->rear].path, path, MAX_NAME_LEN - 1);
queue->items[queue->rear].img = img;
queue->size++;
pthread_cond_signal(&queue->not_empty);
pthread_mutex_unlock(&queue->mutex);
return 0;
}
- 内存管理:
name
和path
使用 strncpy 复制到固定大小的字符数组,MAX_NAME_LEN - 1
确保留空间给终止符,避免缓冲区溢出。img
指针直接赋值,队列不负责分配或释放c_mat_t
或其mat_data
,该操作由创建实际参数时负责。- 注意:笔者调用前已经确保
img
和mat_data
在入队期间保持有效,且在队列使用期间不被释放。
出队图像:
int dequeue(ImageQueue *queue, ImageInfo *info) {
pthread_mutex_lock(&queue->mutex);
while (queue->size == 0) {
pthread_cond_wait(&queue->not_empty, &queue->mutex);
}
*info = queue->items[queue->front];
queue->front = (queue->front + 1) % MAX_QUEUE_SIZE;
queue->size--;
pthread_cond_signal(&queue->not_full);
pthread_mutex_unlock(&queue->mutex);
return 0;
}
- 内存管理:
info
接收ImageInfo
的副本,name
和path
的字符数组被复制,img
指针被复制。- 注意:出队后,
info->img
指向的c_mat_t
和mat_data
仍由调用者管理。此时笔者通常会才使用完ImageInfo *info
后(如 AI 推理)手动释放,如下所示:(这样的好处在于只是用该张图片的原来存储的堆空间,便于内存管理,以及避免深拷贝带来的时间成本)
if (info.img->mat_data)
{
image_free(info.img);
info.img->mat_data = NULL;
}
3. 内存管理注意事项
3.1 c_mat_t
和 mat_data
的生命周期
c_mat_t
和其mat_data
由调用者分配和释放,队列仅存储指针。这要求调用者在入队和出队时确保内存的有效性。这意味着调用者需要在调用入队前分配,出队后释放,这一点很重要。
3.2 队列销毁时的内存清理:
destroy_queue
仅释放 POSIX 资源,未清理items
中残留的img
指针。- 问题:如果队列销毁时仍有图像未出队,
c_mat_t
和mat_data
可能泄漏。 - 改进办法:
- 在
destroy_queue
中遍历items
,释放有效的img
和mat_data
(需调用者提供释放函数)。 - 或者记录所有入队的
c_mat_t
,在销毁时统一释放。
- 在
4. 总结
该图像缓冲队列通过固定大小的循环缓冲区和线程同步机制,实现了高效的图像数据管理。然而,内存管理的细节至关重要,尤其是在 c_mat_t
和 mat_data
的生命周期管理、字符数组截断、以及队列销毁时的清理等方面。遵循上述注意事项和优化建议,可以显著提高队列的稳定性和可维护性。