读取互斥量范围之外的易失性变量,而不是 std::atomic

reading volatile variable outside of scope of a mutex as opposed to std::atomic

我正在尝试针对 SPSC 队列中的消费者延迟进行优化,如下所示:

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;
};

此处的消费者尽可能避免为互斥锁支付 locking/unlocking,并在锁定互斥锁之前进行检查。

问题是:我是否绝对必须使用 TYPE = std::atomic<bool> 或者我可以节省原子操作和 阅读 一个 volatile bool 可以吗?

It's known that a volatile variable per se doesn't guarantee thread safety,但是,std::mutex::lock()std::mutex::unlock() 提供了一些内存排序保证。我可以依靠他们对 volatile bool has_new_messages 进行更改,最终对 mutex 范围 之外的消费者线程 可见吗?


更新:按照@Peter Cordes的,我重写如下:

    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();
        }
    }

不能是普通的bool。 reader 中的自旋循环将优化为如下所示:
if (!has_new_messages) infinite_loop; 因为编译器可以将负载提升到循环之外,因为它允许假设它不会异步更改。


volatile 在某些平台上工作(包括大多数主流 CPUs,例如 x86-64 或 ARM)作为 atomic loads/stores 的蹩脚替代品 memory_order_relaxed,对于 的类型。即无锁原子 load/store 使用与普通 load/store.

相同的 asm

我最近写了一篇比较volatile with relaxed atomic for an interrupt handler的回答,但实际上并发线程基本相同。 has_new_messages.load(std::memory_order_relaxed) 编译为您在普通平台上从 volatile 获得的相同 asm(即没有额外的防护指令,只是一个普通的加载或存储),但它是合法的/可移植的 C++。

你可以而且应该只在互斥量之外使用 std::atomic<bool> has_new_messages;mo_relaxed loads/stores, 如果对 [=12 做同样的事情=] 会很安全。

您的编写者可能应该在 after 释放互斥锁后标记 ,或者在关键部分的末尾使用 memory_order_release 存储。 reader 打破自旋循环并在编写者尚未实际释放互斥锁时尝试获取互斥锁是没有意义的。

顺便说一句,如果你的 reader 线程正在 has_new_messages 上旋转等待它变为真,你应该在 x86 上的循环中使用 _mm_pause() 来保存电源并避免内存顺序错误推测管道 clear 当它确实发生变化时。还可以考虑在旋转几千次后回退到 OS 辅助 sleep/wake。请参阅 , and for more about memory written by one thread and read by another, see (包括一些内存顺序错误推测结果。)


或者更好,使用无锁 SPSC 队列;有很多使用固定大小的环形缓冲区的实现,如果队列不是满的或空的, reader 和 writer 之间没有争用。如果你把 reader 和 writer 的原子位置计数器安排在不同的缓存行中,那应该是好的。


changes to volatile bool has_new_messages to be eventually visible to the consumer thread

这是一个常见的误解。任何商店都将 非常 很快对所有其他 CPU 核心可见,因为它们都共享一个连贯的缓存域,并且商店会尽快提交给它而无需任何隔离说明。

。最坏的情况可能是大约一微秒,在一个数量级内。通常较少。

并且volatileatomic确保在编译器生成的asm中实际上会有一个存储。

(相关:当前的编译器基本上根本不优化 atomic<T>;所以 atomic 基本上等同于 volatile atomic。但即使没有它,编译器无法跳过执行存储或将负载提升到自旋循环之外。)