滤镜多线程的创建与启动
作者:罗上文,微信:Loken1,公众号:FFmpeg弦外之音
FilterGraph 虽然用的是 ScliceThread 多线程,但它并不是直接使用的,它是在 ScliceThread 基础上又封装了一层 ThreadContext,具体实现在 libavfilter/pthread.c 里面。
它们之间的数据结构的关系如下:

你可以把 WorkerContext 理解成线程实例,就是如果创建了 8 个线程,那就会有 8 个 WorkerContext 。
我挑一些重点的字段来讲解一下:
1,ThreadContext::ctx
ScliceThread 多线程并不是单独给某个一滤镜用的,它是 FilterGraph 里面的全局线程。例如 overlay 滤镜用完 ScliceThread 多线程之后,又可以给 chromakey 滤镜用。
所以在 ThreadContext 里面需要一个 ctx 记录来记录,当前正在使用多线程的是哪个滤镜。
2,ThreadContext::func
在 overlay 滤镜里面,这个 func() 函数指针指向的是 滤波函数 blend_slice(),赋值的代码如下:

这些代码里面有好多函数指针,为了准确找到对应的函数,我通常是开断点调试来找的。
上图中的 ctx->internal->execute() 指向 thread_execute(),而 s->blend_slice() 指向 blend_slice_yuv420()
3,AVScliceThread::main_func()、AVScliceThread::worker_func()
worker_func() 可以理解为分片任务函数,这个函数可以由多个线程同时执行的。
main_func() 可以理解为主函数,只有一个线程会执行的 main() 函数。
4,AVScliceThread::nb_threads
nb_threads 指的是有多少个线程可以执行 work 任务。但 nb_threads 并不是指 WorkerContext 的数量,nb_threads 有可能会比 WorkerContext 的数量少 1。
这句话听起来好像有点矛盾,但其实是这样的,因为不只是 work 线程可以执行 work 任务,主线程也可能会去执行 work 任务。如下:

假设现在有 8 个 WorkerContext,加上主线程,那就是有 9 个线程都可以执行 work 任务,所以 nb_threads 等于 9 。
我说的 work 任务只是 Sclice 层面的描述。在滤镜层面,work 任务就是 滤镜处理。在编解码器层面, work 任务就是编解码。
5,AVScliceThread::finished
finished 字段代表 AVScliceThread 实例是否已经结束,这个字段是为了结束 work 线程,不管有没有 job 需要处理,只要 finished 设置为 1 了,就立即退出 work 线程,如下:

只有在 avpriv_slicethread_free() 的时候才会把 finished 设置为 1。
6,AVScliceThread::done
读者不要被 done 这个单词的表明意思误导,以为它代表 work 任务是否已经完成,完成了就设置为 1。不要把这个字段理解成 完不完成。
done 字段的作用仅仅是为了能同步 work 线程与主线程,如下:

当 run_jobs() 返回 1 的时候,代表执行完了最后一个 job 任务。
因为主线程也可以执行 run_jobs(),run_jobs() 就是 work 任务。所以只有两种可能,要不就是 主线程执行了 最后一个 job,要不就是 8 个 work 线程里面的其中一个线程执行了 最后一个 job。
假设是 work 线程执行了 最后一个 job,那主线程执行的就不是最后一个 job,主线程就需要等 work 线程完成任务。work 线程完成任务之后就会把 done 设置为 1,然后就可以唤醒主线程,但是主线程后续又会把 done 设置为原始状态 0。
假设是 主线程 执行了 最后一个 job,那 is_last 就等于 1,work 线程里面的 run_jobs() 永远不会返回 1,这种情况下,下面这两段代码都不会执行。
if (!is_last) {
...不会执行的代码...
}
if (run_jobs(ctx)) {
...不会执行的代码...
}
所以由始至终,ctx->done 都是 0,所以如果你把 done 理解成完成任务的意思,就感觉很奇怪。明明任务完成了,done 的值在这种情况下没改动过。
所以我个人喜欢把 done 理解成一个多线程同步的一个字段而已。
run_jobs() 里面的逻辑是比较复杂的,可能是因为它是一个通用的设计,要兼顾到编码器,解码器的需求。
run_jobs() 里面有一些难点,以这条命令为例讲一下。
ffmpeg -an -i juren-30s.mp4 -an -i juren-30s-small.mp4 -filter_complex "[0:v][1:v]overlay=18:20" out.mp4
第一个难点:
run_jobs() 里面的 ctx->current_job 并不是从 0 或者 1 开始,他这个变量一开始就赋值为 nb_active_threads,如下:

我是 8 核的机器,所以 nb_active_threads 等于 9,所以 ctx->current_job 是从 9 开始的。通常 nb_jobs 又等于 nb_active_threads。
所以 run_jobs() 里面的 while(...) 循环从来没有成立过,只执行了一次 work_func()。
我也不知道他为什么这么写,为什么 ctx->current_job 一开始就赋值为 nb_active_threads。
第二个难点:
run_jobs() 的返回值判断,为什么要减去 1?
return current_job == nb_jobs + nb_active_threads - 1
答:这是因为 atomic_fetch_add_explicit() 函数返回的是旧值,是 +1 之前的值。
滤镜里面其实没有完成用上 ScliceThread 的所有功能跟参数,如下:

可以看到 threadnr 这个变量根本没有用,滤镜的滤波函数只需要知道 jobnr 跟 nb_jobs 就能执行任务了。
而在我们上面那条 overlay 命令里面,worker_func() 被执行了 9 次,9 个线程各执行一遍,jobnr 是从 0 递增到 8 传递给 worker_func() 的
单个 worker 线程在 run_jobs() 里面并没有循环执行 worker_func(),我也不知道什么场景下会循环执行,也不知道他为什么这么写。
我这里只是把我调试 overlay 命令的情况说一下。
滤镜里面创建多线程的函数流程如下:

这里面有很多 互斥锁跟条件变量 的代码,这个是 Linux 开发的常识,就不讲解了,推荐阅读《Unix环境高级编程》
滤镜里面执行 多线程滤波 的流程如下:

