我正在尝试针对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;
};

此处的消费者试图避免为互斥锁的锁定/解锁支付费用,并在锁定互斥锁之前进行检查。

问题是:我是否绝对必须使用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的advice之后,我将其重写如下:
    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。阅读器中的自旋循环将优化为以下形式:if (!has_new_messages) infinite_loop;,因为编译器可以将负载提升到循环之外,因为可以假定它不会异步更改。
volatile在某些平台(包括大多数主流CPU,例如x86-64或ARM)上工作,可以作为"naturally" atomic (e.g. atomic or memory_order_relaxed , because the ABI gives them natural alignment)类型的int加载/存储bool的替代品。即无锁原子加载/存储使用与正常加载/存储相同的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_relaxedvolatile加载/存储在互斥锁之外使用,

在释放互斥锁之后,您的编写者可能应该标记该标志,或者可能在关键部分的末尾使用memory_order_release存储。让读者脱离旋转循环并在作者尚未实际释放互斥体的情况下尝试使用互斥体是没有意义的。

顺便说一句,如果您的阅读器线程在has_new_messages上旋转以等待变为真,则应当在x86的循环中使用_mm_pause()来节省功耗,并避免内存顺序错误的推测管道在更改时清除。还考虑旋转几千次后退回至操作系统辅助的睡眠/唤醒。请参阅What does __asm volatile ("pause" ::: "memory"); do?,有关由一个线程编写并由另一个线程读取的内存的更多信息,请参见What are the latency and throughput costs of producer-consumer sharing of a memory location between hyper-siblings versus non-hyper siblings?(包括一些内存顺序的错误推测结果。)

或者更好的方法是使用无锁的SPSC队列。有很多使用固定大小的环形缓冲区的实现,如果队列不满或为空,则读写器之间不会发生争用。如果将事物安排到原子位置计数器,以使读取器和写入器位于单独的缓存行中,那应该很好。



这是一个普遍的误解。任何一个存储都将很快对所有其他CPU内核可见,因为它们都共享一个一致的缓存域,并且存储将尽快提交给该存储域,而无需任何防护指令。

If I don't use fences, how long could it take a core to see another core's writes?。最坏的情况可能是一个数量级内大约一微秒。通常较少。
volatileatomic确保在编译器生成的asm中确实存在存储。

(相关:当前的编译器基本上根本不优化atomic<T>;因此atomic基本上等效于volatile atomicWhy don't compilers merge redundant std::atomic writes?。但是即使没有这一点,编译器也无法跳过存储或提升旋转循环中的负载。)

关于c++ - 读取互斥范围之外的volatile变量,而不是std::atomic,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/51624607/

10-16 19:16