问题
I have below a SPSC queue for my logger.
It is certainly not a general-use SPSC lock-free queue.
However, given a bunch of assumptions around how it will be used, target architecture etc, and a number of acceptable tradeoffs, which I go into detail below, my questions is basically, is it safe / does it work?
- It will only be used on
x86_64
architecture, so writes touint16_t
will be atomic. - Only the producer updates the
tail
. - Only the consumer updates the
head
. - If the producer reads an old value of
head
, it will look like there is less space in the queue than reality, which is an acceptable limitation in the context in which is is used. - If the consumer reads an old value of
tail
, it will look like there is less data waiting in the queue than reality, again an acceptable limitation.
The limitations above are acceptable because:
- the consumer may not get the latest
tail
immediately, but eventually the latesttail
will arrive, and queued data will be logged. - the producer may not get the latest
head
immediately, so the queue will look more full than it really is. In our load testing we have found the amount we log vs the size of the queue, and the speed at which the logger drains the queue, this limitation has no effect - there is always space in the queue.
A final point, the use of volatile
is necessary to prevent the variable which each thread only reads from being optimised out.
My questions:
- Is this logic correct?
- Is the queue thread safe?
- Is
volatile
sufficient? - Is
volatile
necessary?
My queue:
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
};
回答1:
Is this logic correct?
Yes.
Is the queue thread safe?
No.
Is volatile sufficient? Is volatile necessary?
No, to both. Volatile is not a magic keyword that makes any variable threadsafe. You still need to use atomic variables or memory barriers for the indexes to ensure memory ordering is correct when you produce or consume an item.
To be more specific, after you produce or consume an item for your queue you need to issue a memory barrier to guarantee that other threads will see the changes. Many atomic libraries will do this for you when you update an atomic variable.
As an aside, use "was_empty" instead of "is_empty" to be clear about what it does. The result of this call is one instance in time which may have changed by the time you act on its value.
来源:https://stackoverflow.com/questions/27139260/spsc-lock-free-queue-without-atomics