我的记录器有一个SPSC队列。
它当然不是通用的 SPSC 无锁队列。
但是,鉴于围绕如何使用它、目标架构等的一系列假设,以及一些可接受的权衡,我将在下面详细介绍,我的问题基本上是,它是否安全/它有效吗?
- 它只会在
x86_64
体系结构上使用,因此写入uint16_t
将是原子的。 - 只有制作者更新
tail
。 - 只有消费者更新
head
。 - 如果生产者读取旧值
head
,看起来队列中的空间比实际少,这在使用上下文中是可以接受的限制。 - 如果消费者读取旧的值
tail
,看起来队列中等待的数据比实际少,这又是一个可以接受的限制。
上述限制是可以接受的,因为:
- 使用者可能不会立即获得最新的
tail
,但最终最新的tail
将到达,并且将记录排队的数据。 - 生产者可能不会立即获得最新的
head
,因此队列看起来会比实际更满。在我们的负载测试中,我们发现了我们记录的数量与队列的大小,以及记录器排空队列的速度,此限制不起作用 - 队列中始终有空间。
最后一点,使用 volatile
是必要的,以防止每个线程仅读取的变量被优化。
我的问题:
- 这个逻辑正确吗?
- 队列线程安全吗?
volatile
够吗?volatile
有必要吗?
我的队列:
class LogBuffer
{
public:
bool is_empty() const { return head_ == tail_; }
bool is_full() const { return uint16_t(tail_ + 1) == head_; }
LogLine& head() { return log_buffer_[head_]; }
LogLine& tail() { return log_buffer_[tail_]; }
void advance_head() { ++head_; }
void advance_hail() { ++tail_; }
private:
volatile uint16_t tail_ = 0; // write position
LogLine log_buffer_[0xffff + 1]; // relies on the uint16_t overflowing
volatile uint16_t head_ = 0; // read position
};
这个逻辑正确吗?
是的。
队列线程安全吗?
不。
挥发性是否足够?挥发性是必要的吗?
不,两者都有。易失性不是一个使任何变量线程安全的神奇关键字。您仍然需要对索引使用原子变量或内存屏障,以确保在生成或使用项目时内存排序正确。
更具体地说,在为队列生成或使用项目后,需要发出内存屏障,以保证其他线程将看到更改。许多原子库会在您更新原子变量时为您执行此操作。
顺便说一句,使用"was_empty"而不是"is_empty"来明确它的作用。此调用的结果是一个实例,在您对其值执行操作时,该实例可能已更改。