【问题标题】:reading volatile variable outside of scope of a mutex as opposed to std::atomic在互斥体范围之外读取 volatile 变量,而不是 std::atomic
【发布时间】:2019-01-08 12:18:24
【问题描述】:

我正在尝试优化 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&lt;bool&gt; 还是我可以节省原子操作和阅读 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();
        }
    }

【问题讨论】:

  • 你可以依赖互斥锁/解锁,你不需要 volatile ,但不能超出范围
  • 为什么has_new_messages使用模板类型?不应该只是bool吗?还是这样你就可以做一个boolvolatile bool 版本?
  • @PeterCordes,是的,只是为了演示目的。这是针对bool vs volatile bool vs std::atomic&lt;bool&gt;
  • xchg 上旋转是次优的,而pause 是只读的,我认为。也许if(exchange) { ... } else { _mm_pause(); } 也可以。通常你想旋转只读,如果你的只读检查表明它应该工作,只有xchg。但是与单独的读/写相比,交换似乎没有优势,如果没有其他线程可以从你下面消耗它。
  • 在我看来,更好的锁/信号量应该能够完成整个工作,而不是让缓存线在内核之间弹跳以访问has_new_messages 以及互斥锁。不过,我不知道某种基于计数器的锁是否可以解决问题。

标签: c++ thread-safety x86-64 volatile stdatomic


【解决方案1】:

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


volatile 在某些平台(包括大多数主流 CPU,例如 x86-64 或 ARM)上作为 atomic 使用 memory_order_relaxed 加载/存储的糟糕替代品,适用于 "naturally" atomic (e.g. int or bool, because the ABI gives them natural alignment) 类型。即无锁原子加载/存储使用与正常加载/存储相同的 asm。

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

你可以而且应该只使用 std::atomic&lt;bool&gt; has_new_messages;mo_relaxed 在互斥锁之外加载/存储,如果用 volatile 做同样的事情是安全的。

您的编写者可能应该在释放互斥锁后标记标志,或者在关键部分的末尾使用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 队列;有许多使用固定大小的环形缓冲区的实现,如果队列未满或未为空,则读取器和写入器之间不会发生争用。如果您将读取器和写入器的原子位置计数器安排在不同的缓存行中,那应该很好。


更改为volatile bool has_new_messages最终对消费者线程可见

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

If I don't use fences, how long could it take a core to see another core's writes?。最坏的情况可能是大约一微秒,在一个数量级之内。通常更少。

并且volatileatomic 确保在编译器生成的 asm 中确实存在存储。

(相关:当前的编译器基本上根本不优化atomic&lt;T&gt;;所以atomic基本上等同于volatile atomicWhy don't compilers merge redundant std::atomic writes?。但即使没有,编译器也不能跳过存储或将负载从旋转环中提升出来。)

【讨论】:

  • 谢谢! std::atomic 是否使用宽松的内存顺序使用栅栏?不也是微秒吗?
  • @DevNull:不,不会有任何围栏。 (互斥锁除外)。就像我说的,在 x86-64 上,您将获得与 volatile 相同的 same asm。我重写了最后一节以更清楚地解释它。所以是的,worst 情况可能是 1us(或者可能更糟,发生了非常糟糕的事情),但平均情况可能与您在线程间延迟基准中看到的情况有关。例如从这个使用 GNU C __sync builtins 而不是 stdatomic 的旧测试:mechanical-sympathy.blogspot.com/2011/08/…,Sandybridge 上的内核之间可能有 45ns。
  • 我用改进的代码更新了问题,您能否建议在consume 中使用has_new_messages.exchange(false, std::memory_order_relaxed) 是否安全,因为produce 线程不读取has_new_messages,只是分配给它?谢谢!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-12-06
  • 1970-01-01
  • 2013-01-19
  • 1970-01-01
  • 2021-06-21
  • 1970-01-01
相关资源
最近更新 更多