【问题标题】:Multithreaded Realtime audio programming - To block or Not to block多线程实时音频编程 - 阻止或不阻止
【发布时间】:2015-02-28 13:52:54
【问题描述】:

互联网上的许多人在编写音频软件时说,最重要的是不使用内存分配或阻塞代码,即不使用锁。由于这些是非确定性的,因此可能导致输出缓冲区下溢并且音频会出现故障。

Real Time Audio Progrmaming

当我编写视频软件时,我通常同时使用两者,即在堆上分配视频帧并使用锁和条件变量(有界缓冲区)在线程之间传递。我喜欢它提供的强大功能,因为每个操作都可以使用单独的线程,从而使软件能够最大限度地利用每个内核,从而提供最佳性能。

对于音频,我想做类似的事情,在线程之间传递大约 100 个样本的帧,但是,有两个问题。

  1. 如何在不使用内存分配的情况下生成帧?我想我可以使用预先分配的帧池,但这看起来很乱。

  2. 我知道您可以使用无锁队列,并且 boost 有一个很好的库来执行此操作。这将是在线程之间共享的好方法,但是不断地轮询队列以查看数据是否可用似乎占用了大量的 CPU 时间。

根据我的经验,使用互斥锁实际上并不需要太多时间,只要互斥锁被锁定的部分很短。

什么是实现线程之间传递音频帧的最佳方式,同时将延迟保持在最低限度,不浪费资源并使用相对较少的非确定性行为?

【问题讨论】:

  • 我对音频了解不多,但这听起来像是一个传统的生产者/消费者问题,你看过计算信号量吗?
  • 你还需要线程吗?音频数据比视频便宜很多。
  • @immibis,这取决于您在音频线程中所做的工作。视频通常每个像素只执行很少的操作,而音频合成对每个样本的要求可能非常高。
  • 我同意@immibis。线程输出音频的时间几乎为零,因此您不会真正“最大化”两个内核。单线程会更简单,没有真正的缺点。
  • 我可以向你保证,在这个软件中,使用单线程不是一个选项。

标签: c++ multithreading audio


【解决方案1】:

好像你做了你的研究!您已经确定了可能是音频故障的根本原因的两个主要问题。问题是:这在 10 年前有多少是重要的,而现在只是民间传说和货物崇拜的节目。

我的两分钱:

1.渲染循环中的堆分配:

根据您的处理块有多小,这些开销可能会很大。罪魁祸首是,很少有运行时有每个线程的堆,所以每次你弄乱堆时,你的性能取决于你进程中的其他线程做什么。例如,如果一个 GUI 线程当前正在删除数千个对象,而您同时从音频渲染线程访问堆,您可能会遇到明显的延迟。

使用预先分配的缓冲区编写您自己的内存管理可能听起来很混乱,但最终它只是您可以隐藏在实用程序源中某处的两个函数。由于您通常提前知道分配大小,因此有很多机会微调和优化内存管理。例如,您可以将细分存储为简单的链表。如果操作正确,这样做的好处是您可以再次分配上次使用的缓冲区。这个缓冲区很有可能在缓存中。

如果固定大小的分配器不适合您,请查看环形缓冲区。它们非常适合流式音频的用例。

2.锁定还是不锁定:

我想说的是,如果您可以估计每秒执行的次数少于 1000 到 5000 个,那么这些天使用互斥锁和信号量锁就很好了(在 PC 上,在 Raspberry Pi 等设备上情况有所不同) .如果您保持在该范围以下,则开销不太可能出现在性能配置文件中。

转换为您的用例:例如,如果您使用 48kHz 音频和 100 个样本块,您会在一个简单的两线程消费者/生产者模式中生成大约 960 个锁定/解锁操作。这完全在范围内。如果您完全最大化渲染线程,锁定将不会显示在分析中。另一方面,如果您仅使用 5% 的可用处理能力,则可能会出现锁,但您也不会遇到性能问题 :-)

无锁也是一种选择,但混合解决方案也是如此,它首先尝试一些无锁尝试,然后回退到硬锁定。这样,您将两全其美。网上有很多关于这个话题的好资料。

无论如何:

您应该温和地提高非 GUI 线程的线程优先级,以确保它们在遇到锁时能够迅速退出。阅读什么是优先级反转以及如何避免它也是一个好主意:

https://en.wikipedia.org/wiki/Priority_inversion

【讨论】:

    【解决方案2】:

    '我想我可以使用预先分配的帧池,但这看起来很乱' - 不是真的。要么分配一个帧数组,要么在一个循环中增加新的帧,然后将索引/指针推到阻塞队列中。现在您有了一个自动管理的帧池。需要框架时弹出一个,完成后将其推回。没有连续的 malloc/free/new/delete,没有机会或内存失控,更简单的调试和帧流控制,(如果池用完,请求帧的线程将等待直到帧被释放回池中),所有内置。

    使用数组似乎比新循环更容易/更安全/更快,但更新单个帧确实有一个优势 - 您可以在运行时轻松更改池中的帧数。

    【讨论】:

      【解决方案3】:

      嗯,你为什么要在线程之间传递 100 个样本的帧?

      假设您以 44.1kHz 的标称采样率工作,并在线程之间一次传递 100 个样本,则假定您的线程切换率必须至少为 100 个样本/(44100 个样本/秒 * 2)。 2 代表生产者和消费者。这意味着您每发送 100 个样本就有约 1.13 毫秒的时间片。几乎所有操作系统都以大于 10 毫秒的时间片运行。因此,在现代操作系统上,您不可能在 44.1kHz 的线程之间仅共享 100 个样本的音频引擎。

      解决方案是通过队列或使用更大的帧来缓冲每个时间片的更多样本。大多数现代实时音频 API 使用每个通道 128 个样本(在专用音频硬件上)或每个通道 256 个样本(在游戏控制台上)。

      最终,您的问题的答案大多是您期望的答案...传递指向缓冲区的唯一拥有的指针队列,而不是缓冲区本身;管理在程序启动时分配的固定池中的所有音频缓冲区;并尽可能短的时间锁定所有队列。

      有趣的是,这是音频编程中为数不多的好情况之一,在这些情况下,清除汇编代码具有明显的性能优势。您绝对不希望每个队列锁都出现 malloc 和 free 。如果您了解自己的 CPU,操作系统提供的原子锁定功能总是可以改进的。

      最后一件事:不存在无锁队列。所有多线程“无锁”队列实现都依赖于 CPU 内在屏障或某处的硬比较和交换,以确保每个线程都能独占访问内存。

      【讨论】:

      • 你说“每个缓冲区有更多样本”,但随后给出了更少样本的示例(128 字节可能是 32 个立体声样本左右)
      • 另外,RAII 风格的互斥体不需要动态分配。
      • RAII 中的 A 是“获取”,而不是“分配”。 lock_guard 是 RAII 的主要例子 without any dynamic memory whatsoever
      • 您链接的源代码在第 29 行有一个私有的 Mutex& m,每当 lock_guard 被实例化时,它就会在 lock_guard 内动态分配和复制。
      • 什么动态分配?什么副本?这是一个引用,它在运行时占用与指针相同的空间。它位于lock_guard 内,因此将在堆栈上分配(您的lock_guard 实例是一个自动的又名局部变量,对吗?这就是它的用途)看不到任何动态分配。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2014-07-12
      • 2017-01-21
      • 2021-05-05
      • 1970-01-01
      • 2013-11-07
      • 1970-01-01
      • 2012-10-17
      相关资源
      最近更新 更多