I have a ring buffer that looks like:
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};
};
This data structure is intended to work with two threads: a publisher thread and a consumer thread. The two major functions without passed memory orders to atomics listed below:
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;
}
I know that, at least, I must have tail_.store()
in the Publish()
function with std::memory_order::release
and tail_.load()
with std::memory_order::acquire
in the Consume()
function to create happens before connection between a write to buffer_
and a read buffer_
. Also, I can pass std::memory_order::relaxed
to tail_.load()
in Publish()
and to head_.load()
in Consume()
because the same thread will see the last write to an atomic. Now the functions are something like this:
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;
}
The last step is to put memory orders in the remaining pair: head_.load()
in Publish()
and head_.store()
in Consume()
. I have to have value = std::move(buffer_[curr_head]);
line executed before head_.store()
in Consume()
, otherwise I will have data races in cases when the buffer is full, so, at least, I must pass std::memory_order::release
to that store operation to avoid reorderings. But do I have to put std::memory_order::acquire
in head_.load()
in the Publish()
function? I understand that it will help head_.load()
to see head_.store()
for reasonable time unlike std::memory_order::relaxed
, but if I don't need this guarantee of shorter time to see a side effect of a store operation, can I have a relaxed memory order? If I can't, then why? Completed code:
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;
}
Are memory orders for each atomic correct? Am I right about explanation of use of each memory order in each atomic variable?
release
order on an atomic store than the synchronizes-with ordering between release/acquire. – Trunnionstd::atomic
release/acquire, i.e. the one associated with a memory location. I am not talking about the release fence, i.e. the one not associated with a memory location. If you mean that OP could use a release fence instead ofhead
store-release, then that might be true (I haven't thought about the details.) – Trunnionstd::atomic_thread_fence(std::memory_order_release)
is a release fence, not a release operation. (The standard apparently describes it as a "synchronization operation" in the part you linked, but it's not a "release operation". See preshing.com/20131125/… for more about the difference.) – Overweening