滤镜多线程的创建与启动

作者:罗上文,微信:Loken1,公众号:FFmpeg弦外之音

FilterGraph 虽然用的是 ScliceThread 多线程,但它并不是直接使用的,它是在 ScliceThread 基础上又封装了一层 ThreadContext,具体实现在 libavfilter/pthread.c 里面。

它们之间的数据结构的关系如下:

1-1

你可以把 WorkerContext 理解成线程实例,就是如果创建了 8 个线程,那就会有 8 个 WorkerContext

我挑一些重点的字段来讲解一下:

1,ThreadContext::ctx

ScliceThread 多线程并不是单独给某个一滤镜用的,它是 FilterGraph 里面的全局线程。例如 overlay 滤镜用完 ScliceThread 多线程之后,又可以给 chromakey 滤镜用。

所以在 ThreadContext 里面需要一个 ctx 记录来记录,当前正在使用多线程的是哪个滤镜。

2,ThreadContext::func

在 overlay 滤镜里面,这个 func() 函数指针指向的是 滤波函数 blend_slice(),赋值的代码如下:

1-2

这些代码里面有好多函数指针,为了准确找到对应的函数,我通常是开断点调试来找的。

上图中的 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 任务。如下:

1-3

假设现在有 8 个 WorkerContext,加上主线程,那就是有 9 个线程都可以执行 work 任务,所以 nb_threads 等于 9 。

我说的 work 任务只是 Sclice 层面的描述。在滤镜层面,work 任务就是 滤镜处理。在编解码器层面, work 任务就是编解码。


5,AVScliceThread::finished

finished 字段代表 AVScliceThread 实例是否已经结束,这个字段是为了结束 work 线程,不管有没有 job 需要处理,只要 finished 设置为 1 了,就立即退出 work 线程,如下:

1-5

只有在 avpriv_slicethread_free() 的时候才会把 finished 设置为 1。


6,AVScliceThread::done

读者不要被 done 这个单词的表明意思误导,以为它代表 work 任务是否已经完成,完成了就设置为 1。不要把这个字段理解成 完不完成。

done 字段的作用仅仅是为了能同步 work 线程与主线程,如下:

1-4

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,如下:

1-6

我是 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 的所有功能跟参数,如下:

1-7

可以看到 threadnr 这个变量根本没有用,滤镜的滤波函数只需要知道 jobnrnb_jobs 就能执行任务了。

而在我们上面那条 overlay 命令里面,worker_func() 被执行了 9 次,9 个线程各执行一遍,jobnr 是从 0 递增到 8 传递给 worker_func()

单个 worker 线程在 run_jobs() 里面并没有循环执行 worker_func(),我也不知道什么场景下会循环执行,也不知道他为什么这么写。

我这里只是把我调试 overlay 命令的情况说一下。


滤镜里面创建多线程的函数流程如下:

1-8

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


滤镜里面执行 多线程滤波 的流程如下:

1-8


FFmpeg书籍版权所属@罗上文 2025 all right reserved,powered by Gitbook该文件修订时间: 2026-08-14 15:25:19

results matching ""

    No results matching ""