I'm trying to optimise for consumer latency in an SPSC queue like this:
template <typename TYPE>
class queue
{
public:
void produce(message m)
{
const auto lock = std::scoped_lock(mutex);
has_new_messages = true;
new_messages.emplace_back(std::move(m));
}
void consume()
{
if (UNLIKELY(has_new_messages))
{
const auto lock = std::scoped_lock(mutex);
has_new_messages = false;
messages_to_process.insert(
messages_to_process.cend(),
std::make_move_iterator(new_messages.begin()),
std::make_move_iterator(new_messages.end()));
new_messages.clear();
}
// handle messages_to_process, and then...
messages_to_process.clear();
}
private:
TYPE has_new_messages{false};
std::vector<message> new_messages{};
std::vector<message> messages_to_process{};
std::mutex mutex;
};
The consumer here is trying to avoid paying for locking/unlocking of the mutex if possible and does the check before locking the mutex.
The question is: do I absolutely have to use TYPE = std::atomic<bool>
or I can save on atomic operations and reading a volatile bool
is fine?
It's known that a volatile
variable per se doesn't guarantee thread safety, however, std::mutex::lock()
and std::mutex::unlock()
provide some memory ordering guarantees. Can I rely on them to make changes to volatile bool has_new_messages
to be eventually visible to the consumer thread outside of the mutex
scope?
Update: Following @Peter Cordes' advice, I'm rewriting this as follows:
void produce(message m)
{
{
const auto lock = std::scoped_lock(mutex);
new_messages.emplace_back(std::move(m));
}
has_new_messages.store(true, std::memory_order_release);
}
void consume()
{
if (UNLIKELY(has_new_messages.exchange(false, std::memory_order_acq_rel))
{
const auto lock = std::scoped_lock(mutex);
messages_to_process.insert(...);
new_messages.clear();
}
}
has_new_messages
using the template type? Shouldn't it just bebool
? Or is that so you can do abool
vs.volatile bool
version? – Peter Cordesbool
vsvolatile bool
vsstd::atomic<bool>
. – Dev Nullxchg
is sub-optimal vs. read-only withpause
, I think. Maybeif(exchange) { ... } else { _mm_pause(); }
would be ok, too. Normally you want to spin read-only, and onlyxchg
if your read-only check says it should work. But exchange seems to have no advantage vs. separate read / write, if no other thread can consume it out from under you. – Peter Cordeshas_new_messages
as well as the mutex. I don't know if some kind of counter-based lock could do the trick, though. – Peter Cordes