FFmepg-- 42-ffplay.c 线程安全机制深度分析

文章目录

  • [ffplay.c 线程安全机制深度分析](#ffplay.c 线程安全机制深度分析)
    • 一、整体架构概览
    • 二、互斥锁(Mutex)机制
      • [2.1 为什么需要互斥锁?](#2.1 为什么需要互斥锁?)
      • [2.2 PacketQueue 中的互斥锁](#2.2 PacketQueue 中的互斥锁)
      • [2.3 FrameQueue 中的互斥锁](#2.3 FrameQueue 中的互斥锁)
    • 三、条件变量与等待/唤醒机制
      • [3.1 什么是等待(Wait)和唤醒(Signal)?](#3.1 什么是等待(Wait)和唤醒(Signal)?)
      • [3.2 为什么需要条件变量?](#3.2 为什么需要条件变量?)
      • [3.3 为什么要用 while 而不是 if?](#3.3 为什么要用 while 而不是 if?)
    • [四、PacketQueue 的等待唤醒实现](#四、PacketQueue 的等待唤醒实现)
      • [4.1 生产端:packet_queue_put(唤醒消费者)](#4.1 生产端:packet_queue_put(唤醒消费者))
      • [4.2 消费端:packet_queue_get(等待数据)](#4.2 消费端:packet_queue_get(等待数据))
    • [五、FrameQueue 的等待唤醒实现](#五、FrameQueue 的等待唤醒实现)
      • [5.1 写等待:frame_queue_peek_writable](#5.1 写等待:frame_queue_peek_writable)
      • [5.2 读等待:frame_queue_peek_readable](#5.2 读等待:frame_queue_peek_readable)
      • [5.3 写入完成:frame_queue_push(唤醒读者)](#5.3 写入完成:frame_queue_push(唤醒读者))
      • [5.4 消费完成:frame_queue_next(唤醒写者)](#5.4 消费完成:frame_queue_next(唤醒写者))
    • 六、数据读取线程(read_thread)详解
      • [6.1 主要职责](#6.1 主要职责)
      • [6.2 队列满时的等待](#6.2 队列满时的等待)
      • [6.3 read_thread 的唤醒时机](#6.3 read_thread 的唤醒时机)
    • 七、音频/视频解码线程详解
      • [7.1 音频解码线程(audio_thread)](#7.1 音频解码线程(audio_thread))
      • [7.2 视频解码线程(video_thread)](#7.2 视频解码线程(video_thread))
      • [7.3 decoder_decode_frame 核心解码逻辑](#7.3 decoder_decode_frame 核心解码逻辑)
    • 八、完整时序举例:从读到播的等待唤醒链路
      • 场景:播放器刚启动时的数据流
        • [⏱️ T0:初始状态](#⏱️ T0:初始状态)
        • [⏱️ T1:read_thread 读取到第一个音频包](#⏱️ T1:read_thread 读取到第一个音频包)
        • [⏱️ T2:audio_thread 被唤醒,开始解码](#⏱️ T2:audio_thread 被唤醒,开始解码)
        • [⏱️ T3:sdl_audio_callback 被唤醒,开始播放](#⏱️ T3:sdl_audio_callback 被唤醒,开始播放)
        • [⏱️ T4:队列满时的反向控制](#⏱️ T4:队列满时的反向控制)
        • [⏱️ T5:PacketQueue 满时控制读取速度](#⏱️ T5:PacketQueue 满时控制读取速度)
    • [九、ffplay 中所有条件变量汇总](#九、ffplay 中所有条件变量汇总)
    • 十、总结
      • [🎯 核心设计思想](#🎯 核心设计思想)
      • [📌 关键要点回顾](#📌 关键要点回顾)

ffplay.c 线程安全机制深度分析

基于 FFmpeg 4.2.1 版本 | 互斥锁 · 条件变量 · 等待唤醒 · 生产者消费者模型


一、整体架构概览

ffplay 是一个典型的多线程生产者-消费者模型播放器。它包含以下核心线程:

复制代码
┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│  read_thread │────▶│  PacketQueue │────▶│ audio_thread │
│  数据读取线程  │     │ audioq/videoq│     │ 音频解码线程   │
└──────────────┘     └──────────────┘     └──────────────┘
                             │
                             ▼
                        ┌──────────────┐
                        │ video_thread │
                        │ 视频解码线程   │
                        └──────────────┘
                             │
                             ▼ 解码后数据
                        ┌──────────────┐     ┌──────────────────┐
                        │  FrameQueue  │────▶│ sdl_audio_callback │
                        │ sampq/pictq  │     │   音频播放回调     │
                        └──────────────┘     └──────────────────┘
                             │
                             ▼
                        ┌──────────────┐
                        │ video_refresh│
                        │ 视频显示刷新   │
                        └──────────────┘

💡 核心思想: 通过两级队列(PacketQueue + FrameQueue)解耦了"读取-解码-播放"三个环节,每个环节独立运行在不同线程中,通过互斥锁 保证队列操作的线程安全,通过条件变量实现线程间的等待与唤醒。


二、互斥锁(Mutex)机制

2.1 为什么需要互斥锁?

多个线程同时访问共享资源(如队列)时,如果没有同步机制,会出现竞态条件(Race Condition),导致数据损坏。例如:

  • 读线程正在写入队列尾节点时,解码线程同时读取队头节点
  • 两个线程同时修改队列的 nb_packets 计数
  • 队列链表指针在并发修改下出现断链或环链

2.2 PacketQueue 中的互斥锁

PacketQueue 是存储解码前 AVPacket 的队列,每个队列都有独立的互斥锁:

c 复制代码
typedef struct PacketQueue {
    MyAVPacketList *first_pkt, *last_pkt;  // 队首、队尾指针
    int nb_packets;                        // 包数量
    int size;                              // 队列数据总大小
    int64_t duration;                       // 总播放时长
    int abort_request;                      // 退出请求标志
    int serial;                             // 播放序列号
    SDL_mutex *mutex;                      // 互斥锁(关键!)
    SDL_cond  *cond;                       // 条件变量(下节讲)
} PacketQueue;

packet_queue_put 函数的加锁模式:

c 复制代码
static int packet_queue_put(PacketQueue *q, AVPacket *pkt)
{
    int ret;
    SDL_LockMutex(q->mutex);          // 1. 加锁
    ret = packet_queue_put_private(q, pkt);  // 2. 操作共享资源
    SDL_UnlockMutex(q->mutex);        // 3. 解锁
    if (pkt != &flush_pkt && ret < 0)
        av_packet_unref(pkt);
    return ret;
}

⚠️ 注意: 所有对队列的读、写操作都必须在 SDL_LockMutex 和 SDL_UnlockMutex 之间进行,确保同一时刻只有一个线程能操作队列。

2.3 FrameQueue 中的互斥锁

FrameQueue 是存储解码后 AVFrame 的环形队列,同样有自己的互斥锁:

c 复制代码
typedef struct FrameQueue {
    Frame queue[FRAME_QUEUE_SIZE];
    int rindex;          // 读索引
    int windex;          // 写索引
    int size;            // 当前帧数
    int max_size;        // 最大帧数
    SDL_mutex *mutex;   // 互斥锁
    SDL_cond  *cond;    // 条件变量
    PacketQueue *pktq;   // 关联的packet队列
} FrameQueue;

三、条件变量与等待/唤醒机制

3.1 什么是等待(Wait)和唤醒(Signal)?

等待(Wait): 当线程需要的条件不满足时(比如队列为空,消费者想取数据但没数据可取),线程主动放弃CPU并进入休眠状态,直到条件满足被唤醒。

唤醒(Signal): 当某个线程改变了条件(比如生产者往队列里放入了数据),它通知等待在该条件上的线程:"条件可能满足了,你可以醒来检查了"。

条件变量(Condition Variable): 是实现等待/唤醒机制的同步原语,它总是和互斥锁配合使用。

3.2 为什么需要条件变量?

如果没有条件变量,消费者线程只能用"轮询+休眠"的方式检查队列:

c 复制代码
// 低效的轮询方式(不推荐)
while (队列空) {
    SDL_Delay(10);  // 休眠10ms再检查
}

这种方式的问题:

  • 响应延迟高:数据来了最多要等 10ms 才知道
  • CPU 浪费:频繁唤醒检查,即使队列为空
  • 延迟和效率难以权衡:休眠时间短则CPU占用高,长则响应慢

使用条件变量后,线程会在条件不满足时真正挂起,不占用 CPU,条件满足时被立即唤醒:

c 复制代码
// 高效的条件变量方式(推荐)
SDL_LockMutex(q->mutex);
while (队列空) {
    SDL_CondWait(q->cond, q->mutex);  // 原子地:解锁 + 等待 + 被唤醒后重新加锁
}
// 取数据...
SDL_UnlockMutex(q->mutex);

🔑 关键理解: SDL_CondWait 是一个原子操作 ------它在让线程进入等待的同时,会自动释放互斥锁 ;当被唤醒时,又会自动重新获取互斥锁。这样保证了等待期间其他线程能够修改共享资源。

3.3 为什么要用 while 而不是 if?

这是条件变量使用的经典问题。原因有二:

  1. 虚假唤醒(Spurious Wakeup):条件变量可能在没有被 signal 的情况下自发唤醒(POSIX 允许这种行为)
  2. 条件可能已改变:从被唤醒到获取到锁之间,可能有其他线程已经把数据取走了

所以必须用 while 循环,被唤醒后重新检查条件:

c 复制代码
while (f->size >= f->max_size && !f->pktq->abort_request) {
    SDL_CondWait(f->cond, f->mutex);  // 唤醒后继续循环判断条件
}

四、PacketQueue 的等待唤醒实现

4.1 生产端:packet_queue_put(唤醒消费者)

read_thread 作为生产者,往队列里放入 packet 后,通过 SDL_CondSignal 唤醒等待的解码线程:

c 复制代码
static int packet_queue_put_private(PacketQueue *q, AVPacket *pkt)
{
    MyAVPacketList *pkt1;
    if (q->abort_request) return -1;

    pkt1 = av_malloc(sizeof(MyAVPacketList));
    if (!pkt1) return -1;
    pkt1->pkt = *pkt;
    pkt1->next = NULL;
    pkt1->serial = q->serial;

    // 链表操作:将节点加到队尾
    if (!q->last_pkt)
        q->first_pkt = pkt1;
    else
        q->last_pkt->next = pkt1;
    q->last_pkt = pkt1;

    // 更新队列统计信息
    q->nb_packets++;
    q->size += pkt1->pkt.size + sizeof(*pkt1);
    q->duration += pkt1->pkt.duration;

    // 🔔 发出信号:通知等待中的读线程(解码线程)可以取数据了
    SDL_CondSignal(q->cond);
    return 0;
}

4.2 消费端:packet_queue_get(等待数据)

解码线程作为消费者,从队列取数据。如果队列为空且是阻塞模式(block=1),就进入等待:

c 复制代码
static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block, int *serial)
{
    MyAVPacketList *pkt1;
    int ret;

    SDL_LockMutex(q->mutex);    // 加锁

    for (;;) {                   // 注意是 for(;;) 循环!
        if (q->abort_request) {  // 检查是否要退出
            ret = -1;
            break;
        }

        pkt1 = q->first_pkt;     // 从队头取数据
        if (pkt1) {             // 队列中有数据
            q->first_pkt = pkt1->next;
            if (!q->first_pkt)
                q->last_pkt = NULL;
            q->nb_packets--;
            q->size -= pkt1->pkt.size + sizeof(*pkt1);
            q->duration -= pkt1->pkt.duration;
            *pkt = pkt1->pkt;
            if (serial) *serial = pkt1->serial;
            av_free(pkt1);
            ret = 1;
            break;
        } else if (!block) {    // 非阻塞模式,直接返回
            ret = 0;
            break;
        } else {                // 阻塞模式:进入等待
            // 😴 队列空,线程挂起,等待生产者放入数据后被唤醒
            SDL_CondWait(q->cond, q->mutex);
        }
    }
    SDL_UnlockMutex(q->mutex);  // 解锁
    return ret;
}

五、FrameQueue 的等待唤醒实现

FrameQueue 是环形队列,存在两种等待场景:

场景 调用者 等待条件 被谁唤醒
写等待 解码线程(生产者) 队列已满 (size >= max_size) 播放线程消费后调用 frame_queue_next
读等待 播放线程(消费者) 队列为空 (size - rindex_shown <= 0) 解码线程解码后调用 frame_queue_push

5.1 写等待:frame_queue_peek_writable

解码线程想往 FrameQueue 写入解码后的帧,如果队列满了就等待:

c 复制代码
static Frame *frame_queue_peek_writable(FrameQueue *f)
{
    // 😴 队列满了,等待直到有空位可以写入新帧
    SDL_LockMutex(f->mutex);
    while (f->size >= f->max_size && !f->pktq->abort_request) {
        SDL_CondWait(f->cond, f->mutex);
    }
    SDL_UnlockMutex(f->mutex);

    if (f->pktq->abort_request)
        return NULL;

    return &f->queue[f->windex];  // 返回可写位置的指针
}

5.2 读等待:frame_queue_peek_readable

播放线程想从 FrameQueue 读取可播放的帧,如果队列为空就等待:

c 复制代码
static Frame *frame_queue_peek_readable(FrameQueue *f)
{
    // 😴 队列为空,等待直到有新的可读帧
    SDL_LockMutex(f->mutex);
    while (f->size - f->rindex_shown <= 0 &&
           !f->pktq->abort_request) {
        SDL_CondWait(f->cond, f->mutex);
    }
    SDL_UnlockMutex(f->mutex);

    if (f->pktq->abort_request)
        return NULL;

    return &f->queue[(f->rindex + f->rindex_shown) % f->max_size];
}

5.3 写入完成:frame_queue_push(唤醒读者)

解码线程往队列写入一帧后,更新写索引并唤醒等待的读线程:

c 复制代码
static void frame_queue_push(FrameQueue *f)
{
    if (++f->windex == f->max_size)
        f->windex = 0;
    SDL_LockMutex(f->mutex);
    f->size++;                    // 帧数+1
    SDL_CondSignal(f->cond);     // 🔔 唤醒等待可读的线程(播放线程)
    SDL_UnlockMutex(f->mutex);
}

5.4 消费完成:frame_queue_next(唤醒写者)

播放线程消费掉一帧后,更新读索引并唤醒等待的写线程:

c 复制代码
static void frame_queue_next(FrameQueue *f)
{
    if (f->keep_last && !f->rindex_shown) {
        f->rindex_shown = 1;
        return;
    }
    frame_queue_unref_item(&f->queue[f->rindex]);
    if (++f->rindex == f->max_size)
        f->rindex = 0;
    SDL_LockMutex(f->mutex);
    f->size--;                    // 帧数-1
    SDL_CondSignal(f->cond);     // 🔔 唤醒等待可写的线程(解码线程)
    SDL_UnlockMutex(f->mutex);
}

六、数据读取线程(read_thread)详解

6.1 主要职责

read_thread 是 ffplay 的"数据供应者",主要负责:

  1. 打开输入文件/流,探测媒体格式 (avformat_open_input)
  2. 查找流信息,获取音视频编码参数 (avformat_find_stream_info)
  3. 打开音视频解码器,创建解码线程 (stream_component_open)
  4. 循环读取音视频包并放入各自的 PacketQueue (av_read_frame)
  5. 处理 seek、暂停、退出等控制请求

6.2 队列满时的等待

当音视频 packet 队列总大小超过 MAX_QUEUE_SIZE(15MB) 时,read_thread 会主动进入等待,避免内存暴涨:

c 复制代码
/* 如果队列已满,不需要继续读取数据 */
if (infinite_buffer<1 &&
      (is->audioq.size + is->videoq.size + is->subtitleq.size > MAX_QUEUE_SIZE
    || (stream_has_enough_packets(is->audio_st, is->audio_stream, &is->audioq) &&
        stream_has_enough_packets(is->video_st, is->video_stream, &is->videoq) &&
        stream_has_enough_packets(is->subtitle_st, is->subtitle_stream, &is->subtitleq)))) {
    // 😴 队列满了,等待10ms,或者被 continue_read_thread 唤醒
    SDL_LockMutex(wait_mutex);
    SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);
    SDL_UnlockMutex(wait_mutex);
    continue;
}

💡 continue_read_thread 的作用: 这是一个独立于队列 cond 的条件变量,专门用于控制 read_thread 的读取节奏。当队列满时 read_thread 进入等待,当解码线程消费了足够多的数据包后,通过这个条件变量唤醒 read_thread 继续读取数据。

6.3 read_thread 的唤醒时机

continue_read_thread 在以下情况被触发:

触发场景 代码位置 说明
Seek 请求 stream_seek 函数 用户拖动进度条时,立即唤醒读取线程处理 seek
packet 队列空时 decoder_decode_frame 中 解码线程发现队列空了,通知读取线程加快读取
超时自动唤醒 SDL_CondWaitTimeout 最多等待 10ms,超时后自动醒来检查状态

解码线程发现队列空时唤醒 read_thread 的代码:

c 复制代码
// 在 decoder_decode_frame 中
do {
    if (d->queue->nb_packets == 0)  // 没有数据可读了
        SDL_CondSignal(d->empty_queue_cond);  // 🔔 通知read_thread赶紧读数据

    if (packet_queue_get(d->queue, &pkt, 1, &d->pkt_serial) < 0)
        return -1;
} while (d->queue->serial != d->pkt_serial);

而 empty_queue_cond 实际上就是 continue_read_thread,在 decoder_init 中关联:

c 复制代码
static void decoder_init(Decoder *d, AVCodecContext *avctx,
    PacketQueue *queue, SDL_cond *empty_queue_cond) {
    d->empty_queue_cond = empty_queue_cond;  // 关联条件变量
    // ...
}

// 在 stream_component_open 中调用:
decoder_init(&is->auddec, avctx, &is->audioq, is->continue_read_thread);

七、音频/视频解码线程详解

7.1 音频解码线程(audio_thread)

音频解码线程从 audioq 取 packet,解码后放入 sampq(FrameQueue):

c 复制代码
static int audio_thread(void *arg)
{
    VideoState *is = arg;
    AVFrame *frame = av_frame_alloc();
    Frame *af;
    int got_frame = 0;

    do {
        // 1. 从 PacketQueue 取一个packet并解码(内部会等待)
        if ((got_frame = decoder_decode_frame(&is->auddec, frame, NULL)) < 0)
            goto the_end;

        if (got_frame) {
            // 2. 从 FrameQueue 获取可写位置(队列满了会等待)
            if (!(af = frame_queue_peek_writable(&is->sampq)))
                goto the_end;

            // 3. 填充帧信息(pts、duration、serial等)
            af->pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
            af->serial = is->auddec.pkt_serial;
            af->duration = av_q2d((AVRational){frame->nb_samples, frame->sample_rate});

            av_frame_move_ref(af->frame, frame);

            // 4. 推入队列,唤醒播放线程
            frame_queue_push(&is->sampq);  // 🔔 内部会 SDL_CondSignal
        }
    } while (ret >= 0 || ret == AVERROR(EAGAIN) || ret == AVERROR(EOF));

the_end:
    av_frame_free(&frame);
    return ret;
}

7.2 视频解码线程(video_thread)

视频解码线程的工作模式与音频类似,从 videoq 取 packet 解码,解码后放入 pictq:

c 复制代码
static int video_thread(void *arg)
{
    VideoState *is = arg;
    AVFrame *frame = av_frame_alloc();

    for (;;) {  // 循环取出视频解码的帧数据
        // 1. 获取一帧解码后的视频(内部会从 packet queue 等待取数据)
        ret = get_video_frame(is, frame);
        if (ret < 0) goto the_end;
        if (!ret) continue;

        // 2. 计算pts和duration
        duration = (frame_rate.num && frame_rate.den ?
            av_q2d((AVRational){frame_rate.den, frame_rate.num}) : 0);
        pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);

        // 3. 插入视频帧队列(内部会等待可写空间,写入后唤醒读者)
        ret = queue_picture(is, frame, pts, duration,
                            frame->pkt_pos, is->viddec.pkt_serial);
        av_frame_unref(frame);
    }
the_end:
    av_frame_free(&frame);
    return 0;
}

queue_picture 内部同样使用 FrameQueue 的等待/唤醒机制:

c 复制代码
static int queue_picture(VideoState *is, AVFrame *src_frame, ...)
{
    Frame *vp;

    // 😴 获取可写帧(队列满了会等待)
    if (!(vp = frame_queue_peek_writable(&is->pictq)))
        return -1;

    // 拷贝图像数据、设置各种参数...
    vp->pts = pts;
    vp->duration = duration;
    vp->serial = serial;
    // ...

    // 🔔 推入队列,唤醒 video_refresh 线程
    frame_queue_push(&is->pictq);
    return 0;
}

7.3 decoder_decode_frame 核心解码逻辑

这是解码的核心函数,展示了"从队列取packet → 送解码器 → 拿frame"的完整流程:

c 复制代码
static int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtitle *sub) {
    int ret = AVERROR(EAGAIN);

    for (;;) {
        AVPacket pkt;

        // 第一阶段:尝试从解码器接收已解码的帧
        if (d->queue->serial == d->pkt_serial) {
            do {
                if (d->queue->abort_request) return -1;

                ret = avcodec_receive_frame(d->avctx, frame);
                if (ret >= 0) return 1;  // 解码成功,返回一帧
            } while (ret != AVERROR(EAGAIN));
        }

        // 第二阶段:从 packet 队列取一个 packet 送入解码器
        do {
            if (d->queue->nb_packets == 0)
                SDL_CondSignal(d->empty_queue_cond);  // 🔔 队列空了,通知读取线程

            // 😴 阻塞式取packet,队列为空就等待
            if (packet_queue_get(d->queue, &pkt, 1, &d->pkt_serial) < 0)
                return -1;
        } while (d->queue->serial != d->pkt_serial);

        // 处理flush_pkt(seek时重置解码器)
        if (pkt.data == flush_pkt.data) {
            avcodec_flush_buffers(d->avctx);
            // ...
        } else {
            // 将 packet 送入解码器
            avcodec_send_packet(d->avctx, &pkt);
            av_packet_unref(&pkt);
        }
    }
}

八、完整时序举例:从读到播的等待唤醒链路

让我们通过一个具体场景,串联所有线程的等待与唤醒:

场景:播放器刚启动时的数据流

⏱️ T0:初始状态
  • read_thread 刚启动,开始读取文件
  • audio_thread 启动后调用 packet_queue_get,发现 audioq 为空 → 进入等待(SDL_CondWait)
  • video_thread 同理,在 videoq 的 cond 上等待
  • sdl_audio_callback 调用 frame_queue_peek_readable,sampq 为空 → 进入等待
⏱️ T1:read_thread 读取到第一个音频包

read_thread 调用 av_read_frame 读取到一个音频 packet

调用 packet_queue_put(&is->audioq, pkt) → 加锁 → 放入队列 → SDL_CondSignal(q->cond) 唤醒 audio_thread → 解锁

⏱️ T2:audio_thread 被唤醒,开始解码

audio_thread 从 SDL_CondWait 返回,重新获取锁

发现队列不为空,取出 packet,解锁

调用 avcodec_send_packet 送解码器 → avcodec_receive_frame 得到一帧

调用 frame_queue_peek_writable(&is->sampq) → 队列有空位 → 返回可写指针

填充帧数据 → 调用 frame_queue_push → 加锁 → size++ → SDL_CondSignal 唤醒音频播放线程 → 解锁

⏱️ T3:sdl_audio_callback 被唤醒,开始播放

播放回调从 frame_queue_peek_readable 的等待中返回

读取一帧音频数据 → 拷贝到 SDL 音频缓冲区 → 调用 frame_queue_next

frame_queue_next 内部:size-- → SDL_CondSignal 唤醒可能在等待写空间的 audio_thread

⏱️ T4:队列满时的反向控制

如果解码速度 > 播放速度,sampq 逐渐被填满

audio_thread 调用 frame_queue_peek_writable → 发现 size == max_size → 进入等待

直到播放线程消费一帧后,调用 frame_queue_next 发出 signal,audio_thread 才被唤醒继续写入

⏱️ T5:PacketQueue 满时控制读取速度

如果读取速度 > 解码速度,audioq/videoq 大小超过 MAX_QUEUE_SIZE

read_thread 检测到队列满 → 调用 SDL_CondWaitTimeout(is->continue_read_thread, ...) 进入等待

解码线程消费 packet 时,发现 nb_packets == 0 → SDL_CondSignal(d->empty_queue_cond) 唤醒 read_thread

read_thread 被唤醒后继续读取数据


九、ffplay 中所有条件变量汇总

条件变量 所属结构 等待者 唤醒者 等待条件
audioq.cond PacketQueue audio_thread read_thread 音频packet队列为空
videoq.cond PacketQueue video_thread read_thread 视频packet队列为空
sampq.cond FrameQueue audio_thread / sdl_audio_callback sdl_audio_callback / audio_thread 队列满(写等待)/ 队列空(读等待)
pictq.cond FrameQueue video_thread / video_refresh video_refresh / video_thread 队列满(写等待)/ 队列空(读等待)
continue_read_thread VideoState read_thread 解码线程 / stream_seek packet队列总大小超过上限

十、总结

🎯 核心设计思想

  1. 生产者-消费者模型:通过两级队列(PacketQueue + FrameQueue)解耦读取、解码、播放三个环节
  2. 互斥锁保护共享资源:每个队列都有独立的 mutex,所有对队列的操作都在锁保护下进行
  3. 条件变量实现高效等待:避免轮询,线程在条件不满足时挂起,满足时被立即唤醒
  4. 流量控制:通过队列大小上限 + continue_read_thread,实现了从播放端反压到读取端的流量控制
  5. 优雅退出:abort_request 标志 + CondSignal 配合,确保线程能及时响应退出请求

📌 关键要点回顾

  • 等待(Wait):线程主动挂起,释放CPU,直到条件满足被唤醒。总是和互斥锁配合使用。
  • 唤醒(Signal):改变条件的线程通知等待者"条件可能满足了",但不保证条件一定满足。
  • 为什么用 while 循环:防止虚假唤醒和条件变化,唤醒后必须重新检查条件。
  • SDL_CondWait 的原子性:等待时自动解锁,唤醒时自动重新加锁。
  • 两级队列的作用:PacketQueue 解耦"读-解码",FrameQueue 解耦"解码-播放",各环节速度不匹配时通过队列缓冲。
相关推荐
电梯界知识分子1 小时前
浙江台州六层拆迁自建房:建房前期土建交底,规避薄墙预制板误区,预留轮椅通行电梯
运维·服务器·网络·安全·家用电梯
逐光者9331 小时前
滤波器的 R 与 C 如何选择:从截止频率到工程实践
c语言·开发语言·r语言
蓝悦无人机2 小时前
低空赋能森林防火 用科技守护吴中生态安全
科技·安全·无人机·消防·消防无人机
志尊宝2 小时前
Vue3 零基础每日笔记(085):项目上线前 Checklist——环境变量、报错监控、安全自查
前端·javascript·笔记·安全·信息可视化·echarts
水饺编程2 小时前
第2章:Linux 基础,whoami 命令
linux·c语言·汇编
xiejava10182 小时前
【一个人的SOC】(05) -Nmap攻击面扫描-影子资产和暴露面主动发现
安全·安全运营·ai soc
眨眼猫会务智能体2 小时前
眨眼猫会务智能体会务系统的服务响应怎么样?出问题多久有人接?
安全·小程序·会务
小红帽6153 小时前
软考中级信息安全工程师(一)75满分
网络·安全
howdoyoudo2026063 小时前
AI基建融资信用分层:谁被挤出GPU抵押市场?
大数据·人工智能·安全·microsoft·ai·语言模型