我正在努力熟悉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作为旧值 - &gt;线程1已获得锁定
但随后线程2出现并尝试相同。现在因为没有发生“存储同步”(release,seq_cst_acq_rel),所以线程1的spin_lock存储应该是轻松的类型。
但是由此得出它不能与线程2的spin_lock读取同步。这应该使线程2能够从spin_lock读取值0,从而获得锁定
我的错误在哪里?
答案 0 :(得分:5)
您的错误在于忘记spin_lock
是atomic_flag
,因此test_and_set
是原子操作。需要memory_order_acquire
和memory_order_release
来防止读取在锁定操作之前迁移到迁移到解锁之后的写入。锁本身受原子性保护,原子性始终包括可见性。
答案 1 :(得分:2)
test_and_set
操作被指定为具有特殊特征的读 - 修改 - 写操作,其中一个是:
原子读 - 修改 - 写操作应始终读取与读 - 修改 - 写操作相关的写操作之前写入的最后一个值(按修改顺序)。 [n3337§29.3/ 12]
这也是为什么fetch_add
有效的原因,而简单的加载操作不需要读取修改顺序中的最新值。
答案 2 :(得分:2)
对于给定的原子变量,它有一个“修改顺序”。一旦线程1 test_and_sets值从0到1,线程2就不可能看到0。
内存顺序会影响所有其他内存地址的“同步”。如果一个线程使用memory-order_release修改一个原子变量,那么任何读取同一个变量的线程都会通过memory_order_acquire“看到”每个内存改变它发布之前的第一个线程。
获得和释放不是关于原子的。它是关于确保成功锁定自旋锁的每个线程“看到”之前锁定它的每个线程的变化。
修改顺序是使算法无锁的关键。线程1和线程2都试图对同一个变量执行test_and_set,因此根据规则,一个修改“在另一个之前发生”。因为test_and_set“发生在”之前“另一个”进入“进展”,所以至少一个线程必须始终取得进展。这是lockfree的定义