我的情况是这样的:在 Linux 机器上,我有一个共享内存区域,其中包含音频样本的环形缓冲区。该环形缓冲区的消费者是一个硬实时 Xenomai 音频回调,因此不允许执行任何类型的互斥锁定等。生产者是一个常规 Linux 进程,通过
SCHED_FIFO
调度程序进行调度,但除此之外允许为所欲为。
这是我用来实现共享内存环缓冲区的 C++ 类的(非常简化的)实现:
const size_t NUM_SAMPLES_PER_RENDER_BUFFER = 1024; // producer renders 1024 audio samples at a time
const size_t NUM_RENDER_BUFFERS = 4; // quadruple-buffering, for now
struct SharedMemoryAudioData
{
public:
std::atomic<uint64_t> _totalSamplesProduced; /* since program start */
std::atomic<uint64_t> _totalSamplesConsumed; /* since program start */
float _samplesRingBuffer[NUM_SAMPLES_PER_RENDER_BUFFER*NUM_RENDER_BUFFERS];
};
...消费者回调的操作如下(伪代码):
void RealTimeAudioCallback(float * writeSamplesToHere, uint32 numSamplesToWrite)
{
const size_t ringBufSize = NUM_SAMPLES_PER_RENDER_BUFFER*NUM_RENDER_BUFFERS;
size_t readIdx = (size_t) (sharedData._totalSamplesConsumed.load() % ringBufSize);
for (size_t i=0; i<numSamplesToWrite; i++)
{
writeSamplesToHere[i] = sharedData._samplesRingBuffer[readIdx++];
if (readIdx >= ringBufSize) readIdx = 0;
}
sharedData._totalSamplesConsumed = sharedData._totalSamplesConsumed.load() + numSamplesToWrite;
}
生产者代码稍微复杂一些;它定期运行并比较
_totalSamplesProduced
和 _totalSamplesConsumed
并确定适合写入共享内存区域的样本数以保持环形缓冲区填满(即,它会写入尽可能多的样本,而不会冒覆盖消费者当前正在读取的环形缓冲区的区域)。
这一切似乎运行良好,并且对
_totalSamplesProduced
和 _totalSamplesConsumed
成员变量的访问是并发安全的,因为它们都是 std::atomic
。
我的问题是关于访问
_samplesRingBuffer
本身的值的安全性。它们也由生产者线程写入并由消费者线程读取,但它们不是 std::atomic
,因此这似乎是技术上未定义的行为。一种解决方案是将数组的类型更改为 std::atomic<float>
,但我怀疑这可能会显着增加性能成本,所以我宁愿不这样做。
我还应该在这里做些什么来帮助使对
_sampleRingBuffer
的共享访问更加并发安全,同时仍然保持高效率? (我意识到制作人总是有可能不及时制作,这会导致欠载并且可能出现音频故障,但实际上似乎不会发生,无论如何我不认为有我能做些什么吗)
问题是,如果生产者不接触
_totalSamplesConsumed
并且消费者不接触 _totalSamplesProduced
:那么他们永远不会同步任何东西。在这种情况下,访问样本确实是一场数据竞赛。
但是,消费者应该阅读
_totalSamplesProduced
,因为它应该限制样本以读取可用的样本。这样做足以同步生产者和消费者,并且可以安全地使用生产者编写的样本。
生产者释放,消费者获取也足够了,
_totalSamplesProduced
,不需要你现在使用的默认顺序一致性。
另一个错误:
sharedData._totalSamplesConsumed =
sharedData._totalSamplesConsumed.load()
+ numSamplesToWrite;
fetch_add
来代替。