5

我正在尝试熟悉 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
和“Concurrency in Action”一书中等价地提到了它。我还在 SO 的某个地方找到了它。

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

4

3 回答 3

6

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

于 2013-02-09T20:58:32.690 回答
3

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

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

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

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

于 2013-09-04T04:02:35.137 回答
2

test_and_set原子标志上的操作被指定为具有特殊特征的读-修改-写操作,其中之一是:

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

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

于 2013-02-09T23:23:15.017 回答