为什么 GPUImage 使用信号量和处理队列而不是带有运行循环的线程?

Why does GPUImage use semaphores and processing queues instead of a thread with a runloop?

据我了解GPUImage进行DAG遍历并使用 信号量以保护 OpenGL 的使用,同时将其与帧缓冲区纹理缓存一起视为一次性资源。

有理由在这里使用信号量吗?他们不会不必要地使情况复杂化吗?它们提供什么好处以及通过为每个过滤器 DAG 使用单独的线程实现而不是在运行循环中的单独线程上使用 运行 会遇到什么样的问题。 是否有特定的设计考虑因素影响了当前 GPUImage 架构的决策?

在使用 OpenGL(ES) 上下文时,如果您一次从多个线程访问它,就会发生不好的事情。您可以简单地在主线程上执行所有渲染和交互代码,但这会干扰您的 UI 并会在 UI 事件期间停止任何图像或视频处理(例如下拉菜单)。还有 significant performance advantages 在后台线程上进行 OpenGL(ES) 渲染。

因此,您需要一种方法来在后台线程上执行 OpenGL(ES) 渲染,同时仍然防止同时访问。手动创建线程和锁是实现此目的的一种方法,但锁具有显着的性能开销,正确管理手动创建的线程会增加大量代码(并且有可能浪费资源)。

一次一个块的 Grand Central Dispatch 队列是一种有效且相对简单的方法,可以提供对共享资源的安全、无锁访问,就像这样。任何您想在上下文中进行 OpenGL(ES) 渲染的地方,只需将其包装在一个块中,以便在上下文的串行调度队列中调度。这使您可以轻松查看这些访问在您的代码中发生的位置,并使您免于维护手动线程、运行循环和锁的性能和代码开销。

我在 my answer here 中讨论了我使用分派信号量的原因,但这是一种有选择地丢弃传入帧以响应负载的方法。

对于这样的串行调度队列,我想确保在任何给定时间我只有一个图像或视频帧在队列中工作。使用单个 GPU,一次渲染多个图像没有任何优势。

但是,如果您的相机以每秒 30-60 帧的速度提供要处理的帧,并且您的处理管道偶尔需要超过 1/30 或 1/60 秒来处理这些图像,你必须做出决定。您是丢弃传入的帧,还是将它们排入队列进行处理?如果是后者,您将不断在队列中建立越来越多的帧,直到耗尽可用的处理和内存资源,并且您还会看到处理中的延迟越来越大。

调度信号量允许我在串行调度队列中已经处理一个帧时立即丢弃帧,并且以高效和安全的方式执行此操作。它也只添加了几行代码,几乎所有代码都在 my answer here 中找到(这在 Swift 3 中更短且更易读)。

我上面描述的架构已经过全面分析,是我找到的满足这些需求的最佳解决方案。多年来,我一直使用它在旧 iOS 硬件上提供分子模型的 60 FPS OpenGL ES 渲染,在 Mac 上进行实时机器视觉处理,并在 iOS 上提供实时视频过滤。考虑到多线程代码可能出错的所有事情,它被证明是非常可靠且易于维护的。 GCD 队列和信号量的开销从未接近我的视频渲染中的性能瓶颈。