文章目录
- [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?
这是条件变量使用的经典问题。原因有二:
- 虚假唤醒(Spurious Wakeup):条件变量可能在没有被 signal 的情况下自发唤醒(POSIX 允许这种行为)
- 条件可能已改变:从被唤醒到获取到锁之间,可能有其他线程已经把数据取走了
所以必须用 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 的"数据供应者",主要负责:
- 打开输入文件/流,探测媒体格式 (
avformat_open_input) - 查找流信息,获取音视频编码参数 (
avformat_find_stream_info) - 打开音视频解码器,创建解码线程 (
stream_component_open) - 循环读取音视频包并放入各自的 PacketQueue (
av_read_frame) - 处理 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队列总大小超过上限 |
十、总结
🎯 核心设计思想
- 生产者-消费者模型:通过两级队列(PacketQueue + FrameQueue)解耦读取、解码、播放三个环节
- 互斥锁保护共享资源:每个队列都有独立的 mutex,所有对队列的操作都在锁保护下进行
- 条件变量实现高效等待:避免轮询,线程在条件不满足时挂起,满足时被立即唤醒
- 流量控制:通过队列大小上限 + continue_read_thread,实现了从播放端反压到读取端的流量控制
- 优雅退出:abort_request 标志 + CondSignal 配合,确保线程能及时响应退出请求
📌 关键要点回顾
- 等待(Wait):线程主动挂起,释放CPU,直到条件满足被唤醒。总是和互斥锁配合使用。
- 唤醒(Signal):改变条件的线程通知等待者"条件可能满足了",但不保证条件一定满足。
- 为什么用 while 循环:防止虚假唤醒和条件变化,唤醒后必须重新检查条件。
- SDL_CondWait 的原子性:等待时自动解锁,唤醒时自动重新加锁。
- 两级队列的作用:PacketQueue 解耦"读-解码",FrameQueue 解耦"解码-播放",各环节速度不匹配时通过队列缓冲。