多线程实时音频编程 - 阻塞或不阻塞
Multithreaded Realtime audio programming - To block or Not to block
在编写音频软件时,网上很多人都说最重要的是不要使用内存分配或阻塞代码,即不要使用锁。由于这些是不确定的,因此可能导致输出缓冲区下溢并且音频会出现故障。
当我编写视频软件时,我通常会同时使用这两种方法,即在堆上分配视频帧并使用锁和条件变量(有界缓冲区)在线程之间传递。我喜欢它提供的强大功能,因为每个操作都可以使用一个单独的线程,允许软件最大限度地利用每个内核,从而提供最佳性能。
对于音频,我想做类似的事情,在线程之间传递大约 100 个样本的帧,但是,有两个问题。
如何在不使用内存分配的情况下生成帧?我想我可以使用预先分配的帧池,但这看起来很乱。
我知道您可以使用无锁队列,并且 boost 有一个很好的库来执行此操作。这将是在线程之间共享的好方法,但不断轮询队列以查看数据是否可用似乎需要大量 CPU 时间。
根据我使用互斥锁的经验,只要锁定互斥锁的部分很短,实际上根本不会花费太多时间。
实现在线程之间传递音频帧的最佳方式是什么,同时将延迟保持在最低水平,不浪费资源并使用相对较少的非确定性行为?
看来你做了调查!您已经确定了可能是音频故障根本原因的两个主要问题。问题是:这在 10 年前有多少重要,如今只是民间传说和货物崇拜节目。
我的两分钱:
1.渲染循环中的堆分配:
根据您的处理块有多小,这些可能会有相当多的开销。罪魁祸首是,很少 运行 次有每线程堆,所以每次你弄乱堆时,你的性能取决于你进程中的其他线程做什么。例如,如果一个 GUI 线程当前正在删除数千个对象,而您 - 同时 - 从音频渲染线程访问堆,您可能会遇到明显的延迟。
使用预分配的缓冲区编写您自己的内存管理可能听起来很混乱,但最终它只是您可以隐藏在实用程序源中某处的两个函数。由于您通常提前知道您的分配大小,因此有很多机会微调和优化您的内存管理。例如,您可以将细分存储为一个简单的链表。如果做得好,这样做的好处是您可以再次分配上次使用的缓冲区。此缓冲区在缓存中的概率非常高。
如果固定大小的分配器对您不起作用,请查看环形缓冲区。它们非常适合流式音频的用例。
2。锁定还是不锁定:
我想说,现在使用互斥锁和信号量锁很好,如果你可以估计你每秒执行的操作少于 1000 到 5000 个(在 PC 上,情况与 Raspberry Pi 等)。如果您保持在该范围以下,则开销不太可能出现在性能配置文件中。
转换为您的用例:例如,如果您使用 48kHz 音频和 100 个样本块,您将在一个简单的双线程 consumer/producer 模式中生成大约 960 lock/unlock 操作。那在范围之内。如果您完全最大化渲染线程,锁定将不会显示在分析中。另一方面,如果您只使用大约 5% 的可用处理能力,锁可能会出现,但您也不会有性能问题:-)
无锁也是一种选择,但混合解决方案也是如此,首先进行一些无锁尝试,然后回退到硬锁定。这样你就会两全其美。网上有很多关于这个主题的好东西。
无论如何:
您应该逐渐提高非 GUI 线程的线程优先级,以确保如果它们 运行 进入锁中,它们可以快速退出。阅读什么是优先级反转以及如何避免它也是一个好主意:
'I suppose I could use a pool of frames that have been pre-allocated but this seems messy' - 不是真的。要么分配一个帧数组,要么在循环中新建帧,然后将 indices/pointers 推到阻塞队列中。现在您有了一个自动管理的帧池。当你需要一个框架时弹出一个,当你完成它时将其推回。没有连续 malloc/free/new/delete,没有机会或内存失控,更简单的调试和帧流控制,(如果池用完,请求帧的线程将等到帧被释放回池中),所有这些都是内置的.
使用数组似乎 easier/safer/faster 而不是新循环,但新的单个帧确实有一个优势 - 您可以在运行时轻松更改池中的帧数。
嗯,为什么要在线程之间传递 100 个样本的帧?
假设您以 44.1kHz 的标称采样率工作,并且在线程之间一次传递 100 个样本,则假定您的线程切换率必须至少为 100 个样本/(44100 samples/s * 2). 2代表生产者和消费者。这意味着每发送 100 个样本,您就有大约 1.13 毫秒的时间片。几乎所有操作系统 运行 的时间片都大于 10 毫秒。因此,不可能在现代 OS.
上以 44.1kHz 的频率在线程之间构建仅共享 100 个样本的音频引擎。
解决方案是通过队列或使用更大的帧在每个时间片中缓冲更多样本。大多数现代实时音频 API 使用每通道 128 个样本(在专用音频硬件上)或每通道 256 个样本(在游戏机上)。
最终,您的问题的答案大多是您期望的答案...传递唯一拥有的缓冲区指针队列,而不是缓冲区本身;管理程序启动时分配的固定池中的所有音频缓冲区;并根据需要尽可能短地锁定所有队列。
有趣的是,这是音频编程中为数不多的好情况之一,其中破坏汇编代码具有明显的性能优势。您绝对不希望每个队列锁都出现 malloc 和 free。如果您知道 CPU.
,操作系统提供的原子锁定功能总是可以改进的
最后一件事:没有无锁队列这样的东西。所有多线程 "lockfree" 队列实现都依赖于 CPU 内在屏障或某处的硬比较和交换,以确保每个线程对内存的独占访问得到保证。
在编写音频软件时,网上很多人都说最重要的是不要使用内存分配或阻塞代码,即不要使用锁。由于这些是不确定的,因此可能导致输出缓冲区下溢并且音频会出现故障。
当我编写视频软件时,我通常会同时使用这两种方法,即在堆上分配视频帧并使用锁和条件变量(有界缓冲区)在线程之间传递。我喜欢它提供的强大功能,因为每个操作都可以使用一个单独的线程,允许软件最大限度地利用每个内核,从而提供最佳性能。
对于音频,我想做类似的事情,在线程之间传递大约 100 个样本的帧,但是,有两个问题。
如何在不使用内存分配的情况下生成帧?我想我可以使用预先分配的帧池,但这看起来很乱。
我知道您可以使用无锁队列,并且 boost 有一个很好的库来执行此操作。这将是在线程之间共享的好方法,但不断轮询队列以查看数据是否可用似乎需要大量 CPU 时间。
根据我使用互斥锁的经验,只要锁定互斥锁的部分很短,实际上根本不会花费太多时间。
实现在线程之间传递音频帧的最佳方式是什么,同时将延迟保持在最低水平,不浪费资源并使用相对较少的非确定性行为?
看来你做了调查!您已经确定了可能是音频故障根本原因的两个主要问题。问题是:这在 10 年前有多少重要,如今只是民间传说和货物崇拜节目。
我的两分钱:
1.渲染循环中的堆分配:
根据您的处理块有多小,这些可能会有相当多的开销。罪魁祸首是,很少 运行 次有每线程堆,所以每次你弄乱堆时,你的性能取决于你进程中的其他线程做什么。例如,如果一个 GUI 线程当前正在删除数千个对象,而您 - 同时 - 从音频渲染线程访问堆,您可能会遇到明显的延迟。
使用预分配的缓冲区编写您自己的内存管理可能听起来很混乱,但最终它只是您可以隐藏在实用程序源中某处的两个函数。由于您通常提前知道您的分配大小,因此有很多机会微调和优化您的内存管理。例如,您可以将细分存储为一个简单的链表。如果做得好,这样做的好处是您可以再次分配上次使用的缓冲区。此缓冲区在缓存中的概率非常高。
如果固定大小的分配器对您不起作用,请查看环形缓冲区。它们非常适合流式音频的用例。
2。锁定还是不锁定:
我想说,现在使用互斥锁和信号量锁很好,如果你可以估计你每秒执行的操作少于 1000 到 5000 个(在 PC 上,情况与 Raspberry Pi 等)。如果您保持在该范围以下,则开销不太可能出现在性能配置文件中。
转换为您的用例:例如,如果您使用 48kHz 音频和 100 个样本块,您将在一个简单的双线程 consumer/producer 模式中生成大约 960 lock/unlock 操作。那在范围之内。如果您完全最大化渲染线程,锁定将不会显示在分析中。另一方面,如果您只使用大约 5% 的可用处理能力,锁可能会出现,但您也不会有性能问题:-)
无锁也是一种选择,但混合解决方案也是如此,首先进行一些无锁尝试,然后回退到硬锁定。这样你就会两全其美。网上有很多关于这个主题的好东西。
无论如何:
您应该逐渐提高非 GUI 线程的线程优先级,以确保如果它们 运行 进入锁中,它们可以快速退出。阅读什么是优先级反转以及如何避免它也是一个好主意:
'I suppose I could use a pool of frames that have been pre-allocated but this seems messy' - 不是真的。要么分配一个帧数组,要么在循环中新建帧,然后将 indices/pointers 推到阻塞队列中。现在您有了一个自动管理的帧池。当你需要一个框架时弹出一个,当你完成它时将其推回。没有连续 malloc/free/new/delete,没有机会或内存失控,更简单的调试和帧流控制,(如果池用完,请求帧的线程将等到帧被释放回池中),所有这些都是内置的.
使用数组似乎 easier/safer/faster 而不是新循环,但新的单个帧确实有一个优势 - 您可以在运行时轻松更改池中的帧数。
嗯,为什么要在线程之间传递 100 个样本的帧?
假设您以 44.1kHz 的标称采样率工作,并且在线程之间一次传递 100 个样本,则假定您的线程切换率必须至少为 100 个样本/(44100 samples/s * 2). 2代表生产者和消费者。这意味着每发送 100 个样本,您就有大约 1.13 毫秒的时间片。几乎所有操作系统 运行 的时间片都大于 10 毫秒。因此,不可能在现代 OS.
上以 44.1kHz 的频率在线程之间构建仅共享 100 个样本的音频引擎。解决方案是通过队列或使用更大的帧在每个时间片中缓冲更多样本。大多数现代实时音频 API 使用每通道 128 个样本(在专用音频硬件上)或每通道 256 个样本(在游戏机上)。
最终,您的问题的答案大多是您期望的答案...传递唯一拥有的缓冲区指针队列,而不是缓冲区本身;管理程序启动时分配的固定池中的所有音频缓冲区;并根据需要尽可能短地锁定所有队列。
有趣的是,这是音频编程中为数不多的好情况之一,其中破坏汇编代码具有明显的性能优势。您绝对不希望每个队列锁都出现 malloc 和 free。如果您知道 CPU.
,操作系统提供的原子锁定功能总是可以改进的最后一件事:没有无锁队列这样的东西。所有多线程 "lockfree" 队列实现都依赖于 CPU 内在屏障或某处的硬比较和交换,以确保每个线程对内存的独占访问得到保证。