【问题标题】:memory ordering with atomic_flag spin lock使用 atomic_flag 自旋锁进行内存排序
【发布时间】:2013-02-09 20:54:12
【问题描述】:

我正在尝试熟悉 c++11 的新内存排序概念,并相信我实际上对它们有很好的掌握,直到我偶然发现了自旋锁的这种实现:

#include <atomic>

namespace JayZ
{
    namespace Tools
    {
        class SpinLock
        {
        private:
            std::atomic_flag spin_lock;
        public:
            inline SpinLock( void ) : atomic_flag( ATOMIC_FLAG_INIT ) {}

            inline void lock( void )
            {
                while( spin_lock.test_and_set( std::memory_order_acquire ) )
                    ;
            }

            inline void unlock( void )
            {
                lock.clear( std::memory_order_release );
            }
        };
    }
}

例如等同于http://en.cppreference.com/w/cpp/atomic/atomic_flag
以及在“行动中的并发”一书中。我还在 SO 的某个地方找到了它。

但我就是不明白为什么它会起作用!
想象一下线程 1 调用 lock() 并且 test_and_set() 返回 0 作为旧值 --> 线程 1 获得了锁。
但随后线程 2 出现并尝试相同。现在由于没有发生“存储同步”(release,seq_cst_acq_rel) 线程 1 到 spin_lock 的存储应该是放松类型。
但由此得出,它不能与线程 2 对 spin_lock 的读取同步。这应该使线程 2 可以从 spin_lock 读取值 0 并因此也获取锁。
我的错在哪里?

【问题讨论】:

    标签: c++ c++11 atomic


    【解决方案1】:

    您的错误在于忘记了spin_lockatomic_flag,因此test_and_set 是一个原子操作。需要memory_order_acquirememory_order_release 来防止读取迁移到锁定操作之前或写入迁移到解锁之后。锁本身受到原子性的保护,原子性始终包括可见性。

    【讨论】:

    • 但如果你说的是真的。这是否意味着,对原子变量的任何宽松存储都将立即对另一个线程可见?绝对不是这样的,不是吗?
    • 对原子变量的任何操作都是原子的,并且对于对同一原子变量执行原子操作的任何其他线程立即可见。这就是原子变量的作用。
    • 你可以说原子变量实际上做了很多人错误地认为 volatile 变量做的事情。
    • @bames53:这不是可见性问题,而是排序问题。没有什么要求这些操作按照代码指定的顺序实际发生。然而,它们在执行时是原子的,并且在发生 之后对其他原子操作可见。 (正如答案所解释的,如果您需要它以特定方式与其他访问交互,则需要指定内存顺序。)
    • @bames53:不存在“无论商店发生什么订单”这样的事情。这些商店是无序的。这并不意味着它们必须以两种可能的顺序之一发生,这意味着它们不会以某种顺序发生。 (您做出了一个无效的假设,即存在一些全局时间概念,并且每次加载或存储都发生在此全局时间线中的某个定义点。不需要该时间模型。)对不同原子变量的操作是无序的,除非您专门强制订购。
    【解决方案2】:

    对于给定的原子变量,它有一个“修改顺序”。一旦线程 1 的 test_and_set 值从 0 变为 1,线程 2 就不可能看到 0。

    内存顺序会影响所有其他内存地址的“同步”方式。如果一个线程使用 memory-order_release 修改了一个原子变量,那么任何使用 memory_order_acquire 读取相同变量的线程都会“看到”第一个线程在释放之前所做的每个内存更改。

    获取和释放与原子无关。这是为了确保成功锁定自旋锁的每个线程“看到”之前锁定它的每个线程的更改。

    修改顺序是使算法无锁的关键。线程 1 和线程 2 都试图对同一个变量执行 test_and_set,因此按照规则,一个修改“发生在”另一个修改之前。因为“发生在”另一个“进展”之前的 test_and_set,所以至少一个线程必须始终取得进展。这就是lockfree的定义

    【讨论】:

      【解决方案3】:

      test_and_set 原子标志上的操作被指定为具有特殊特性的 read-modify-write 操作,其中之一是:

      原子读-修改-写操作应始终读取在与读-修改-写操作关联的写之前写入的最后一个值(按修改顺序)。 [n3337 § 29.3/12]

      这也是为什么 fetch_add 可以工作,而简单的加载操作不需要读取修改顺序中的最新值。

      【讨论】:

        猜你喜欢
        • 2021-06-05
        • 1970-01-01
        • 2014-02-25
        • 2015-08-21
        • 2022-07-06
        • 1970-01-01
        • 1970-01-01
        • 2021-11-28
        • 2021-12-20
        相关资源
        最近更新 更多