分类 软件 下的文章

线程安全的图像缓冲队列

在这篇博客中,笔者将探讨一个最近实现的基于 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;
}
  • 内存管理:
    • namepath 使用 strncpy 复制到固定大小的字符数组,MAX_NAME_LEN - 1 确保留空间给终止符,避免缓冲区溢出。
    • img 指针直接赋值,队列不负责分配或释放 c_mat_t 或其 mat_data,该操作由创建实际参数时负责。
    • 注意:笔者调用前已经确保 imgmat_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 的副本,namepath 的字符数组被复制,img 指针被复制。
    • 注意:出队后,info->img 指向的 c_mat_tmat_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_tmat_data 可能泄漏。
  • 改进办法
    • destroy_queue 中遍历 items,释放有效的 imgmat_data(需调用者提供释放函数)。
    • 或者记录所有入队的 c_mat_t,在销毁时统一释放。

4. 总结

该图像缓冲队列通过固定大小的循环缓冲区和线程同步机制,实现了高效的图像数据管理。然而,内存管理的细节至关重要,尤其是在 c_mat_tmat_data 的生命周期管理、字符数组截断、以及队列销毁时的清理等方面。遵循上述注意事项和优化建议,可以显著提高队列的稳定性和可维护性。