具有单个生产者单一使用者的无锁循环缓冲区

Lockless circular buffer with single producer singular consumer

本文关键字:循环 循环缓冲 缓冲区 使用者 单个 生产者 单一      更新时间:2023-10-16

我有一个永不锁定或分配内存的使用者线程,以及一个可以锁定和分配内存的生产者线程。 我想实现一个两位循环缓冲区,以便能够从生产者向消费者线程提供数据,并且绑定的是,只要没有新数据可供使用,消费者只需重用已经可用的数据。

这就是我现在想出的:

bool newDataAvailable = false;
bool bufferEmpty = true;
foo* currentData = new foo();
foo* newData = new foo();
void consumer() {
while(true) {
currentData->doSomething();
if(newDataAvailable) {
foo* tmp = currentData;
// Objects are swapped so the old one can be reused without additional allocations
currentData = newData;
newData = tmp;
newDataAvailable = false;
bufferEmpty = true;
}
}
}
void producer() {
while(true) {
while(!bufferEmpty) { wait(); }
newData->init();
bufferEmpty = false;
newDataAvailable = true;
}
}

这种幼稚的实现可以吗?我知道读取和写入变量可以是非原子的,所以我应该使用原子存储,但这些可能会导致锁定。这里是否需要使用原子存储? 另外,我想消除生产者中的主动等待,我认为我可以使用std::condition_variable,但它们需要使用互斥体,我负担不起。

编写共享变量而不使用互斥锁的多线程代码很难正确。 请参阅无锁编程简介、无锁缓冲区。

如果您绝对必须避免使用互斥锁,那么我强烈建议您使用预制的无锁队列,例如 Boost.lockfree 或 MPMCQueue 作为轻量级的非助推替代方案。

我知道读取和写入变量可以是非原子的,所以我应该使用原子存储,但这些可能会导致锁定。

对于所有基元类型(不超过 CPU 的本机大小),std::atomic通常是无锁的(不使用互斥锁)。 您可以通过调用std::atomic<T>::is_lock_free来检查std::atomic是否会对给定类型使用互斥锁

这里是否需要使用原子存储?

是的,绝对。您要么需要使用互斥体,要么需要使用原子组学。

另外,我想消除生产者中的活动等待,我认为我可以使用std::condition_variable

当您无法使用互斥锁时,您唯一的选择就是使用旋转锁。 如果上下文中允许这样做,则可以在旋转锁中使用std::this_thread::yield()来减少 CPU 负载。(但是互斥锁可能会更快)

编辑: 只有 2 个原子的潜在解决方案是:

std::atomic<foo*> currentData = new foo();
std::atomic<foo*> newData = new foo();
void consumer() {
foo* activeData = currentData;
while (true) {
activeData->doSomething();
foo* newItem = currentData;
if (newItem != activeData) {
newData = activeData;
activeData = newItem;
}
}
}
void producer() {
while (true) {
foo* reusedData = newData;
if (!reusedData)
continue;
newData = nullptr;
reusedData->init();
currentData = reusedData;
}
}