【发布时间】:2021-12-28 20:51:51
【问题描述】:
我有一个看起来像这样的环形缓冲区:
template<class T>
class RingBuffer {
public:
bool Publish();
bool Consume(T& value);
bool IsEmpty(std::size_t head, std::size_t tail);
bool IsFull(std::size_t head, std::size_t tail);
private:
std::size_t Next(std::size_t slot);
std::vector<T> buffer_;
std::atomic<std::size_t> tail_{0};
std::atomic<std::size_t> head_{0};
static constexpr std::size_t kBufferSize{8};
};
此数据结构旨在与两个线程一起使用:发布者线程和消费者线程。下面列出了没有将内存顺序传递给原子的两个主要函数:
bool Publish(T value) {
const size_t curr_head = head_.load(/* memory order */);
const size_t curr_tail = tail_.load(/* memory_order */);
if (IsFull(curr_head, curr_tail)) {
return false;
}
buffer_[curr_tail] = std::move(value);
tail_.store(Next(curr_tail) /*, memory order */);
return true;
}
bool Consume(T& value) {
const size_t curr_head = head_.load(/* memory order */);
const size_t curr_tail = tail_.load(/* memory order */);
if (IsEmpty(curr_head, curr_tail)) {
return false;
}
value = std::move(buffer_[curr_head]);
head_.store(Next(curr_head) /*, memory order */);
return true;
}
我知道,至少,我必须在Publish() 函数中有tail_.store() 和std::memory_order::release 和tail_.load() 和std::memory_order::acquire 在Consume() 函数中创建发生之前 写入buffer_ 和读取buffer_ 之间的连接。另外,我可以将std::memory_order::relaxed 传递给Publish() 中的tail_.load() 和Consume() 中的head_.load(),因为同一个线程将看到最后一次写入原子。现在函数是这样的:
bool Publish(T value) {
const size_t curr_head = head_.load(/* memory order */);
const size_t curr_tail = tail_.load(std::memory_order::relaxed);
if (IsFull(curr_head, curr_tail)) {
return false;
}
buffer_[curr_tail] = std::move(value);
tail_.store(Next(curr_tail), std::memory_order::release);
return true;
}
bool Consume(T& value) {
const size_t curr_head = head_.load(std::memory_order::relaxed);
const size_t curr_tail = tail_.load(std::memory_order::acquire);
if (IsEmpty(curr_head, curr_tail)) {
return false;
}
value = std::move(buffer_[curr_head]);
head_.store(Next(curr_head) /*, memory order */);
return true;
}
最后一步是将内存订单放入剩余的对中:head_.load() 放入 Publish() 和 head_.store() 放入 Consume()。我必须在Consume() 中的head_.store() 之前执行value = std::move(buffer_[curr_head]); 行,否则在缓冲区已满的情况下我将发生数据竞争,因此,至少,我必须将std::memory_order::release 传递给该存储操作以避免重新排序.但是我必须将std::memory_order::acquire 放入head_.load() 中的Publish() 函数中吗?我知道这将有助于head_.load() 在合理的时间内看到head_.store() 不像std::memory_order::relaxed,但是如果我不需要这种更短的时间来保证看到存储操作的副作用,我可以有一个轻松的记忆命令?如果我不能,那为什么?完成代码:
bool Publish(T value) {
const size_t curr_head = head_.load(std::memory_order::relaxed); // or acquire?
const size_t curr_tail = tail_.load(std::memory_order::relaxed);
if (IsFull(curr_head, curr_tail)) {
return false;
}
buffer_[curr_tail] = std::move(value);
tail_.store(Next(curr_tail), std::memory_order::release);
return true;
}
bool Consume(T& value) {
const size_t curr_head = head_.load(std::memory_order::relaxed);
const size_t curr_tail = tail_.load(std::memory_order::acquire);
if (IsEmpty(curr_head, curr_tail)) {
return false;
}
value = std::move(buffer_[curr_head]);
head_.store(Next(curr_head), std::memory_order::release);
return true;
}
每个原子的内存顺序是否正确?我对每个原子变量中每个内存顺序的使用的解释是对的吗?
【问题讨论】:
-
我不认为没有获取的发布有任何影响。
-
@ShamilMukhetdinov 这比对原子的释放操作要强。见here。
-
@ShamilMukhetdinov 但这只能在同一原子上执行获取操作的线程中观察到(至少在 C++ 内存模型中)。
-
@ShamilMukhetdinov 线程不需要同意操作发生的顺序,据我所知,只有发布/获取连接建立这样的协议。是的,这就是标准中的 synchronizes-with 属性。除了发布/获取之间的 synchronizes-with 排序之外,我找不到
release订单对原子存储的任何其他影响。 -
@ShamilMukhetdinov 我说的是
std::atomic释放/获取,即与内存位置相关联的那个。我不是在谈论释放围栏,即与内存位置无关的释放围栏。如果您的意思是 OP 可以使用发布围栏而不是headstore-release,那么这可能是正确的(我没有考虑过细节。)
标签: c++ multithreading memory-barriers lock-free stdatomic