我的理解是,可以使用C++11 atomics实现自旋锁,使用的是获取-CAS锁定和解锁上的发布存储,如下所示:
class SpinLock {
public:
void Lock() {
while (l_.test_and_set(std::memory_order_acquire));
}
void Unlock() {
l_.clear(std::memory_order_release);
}
private:
std::atomic_flag l_ = ATOMIC_FLAG_INIT;
};
考虑一下它在一个函数中的使用,该函数获取一个锁,然后对某个共享位置执行盲写:
int g_some_int_;
void BlindWrite(int val) {
static SpinLock lock_;
lock_.Lock();
g_some_int_ = val;
lock_.Unlock();
}
我感兴趣的是编译器在将其转换为生成的程序集代码时是如何受到限制的。
我理解为什么对g_some_int_
的写入不能迁移到编译器输出的关键部分的末尾--这意味着获取锁的下一个线程不能看到写入,而下一个线程通过发布/获取顺序来保证写入。
但是,是什么阻止编译器将其移动到锁标志的获取-CAS之前呢?我认为编译器可以在生成程序集代码时重新排序到不同的内存位置。是否有什么特殊的规则可以防止写入在程序顺序之前的原子存储之前被重新排序?
我正在找一个语言律师回答这里,最好涵盖std::atomic
和std::atomic_flag
。
编辑,在评论中添加一些内容,这些评论可能会更清楚地提出问题。问题的核心是:标准的哪一部分要求抽象机器在写到l_
之前必须观察false
是g_some_int_
我怀疑答案要么是“写不能超越潜在的无限循环”,要么是“写不能高于原子写”。也许甚至是“你错了,写作可以被重新排序”。但我想在标准中找到具体的参考资料。
发布于 2014-07-23 08:33:07
假设您有两个使用自旋锁的函数:
SpinLock sl;
int global_int=0;
int read(){
sl.Lock();
int res=global_int;
sl.Unlock();
return res;
}
void write(int val){
sl.Lock();
global_int=val;
sl.Unlock();
}
如果两个对BlindWrite
的调用同时发生在不同的线程上,那么一个(称为A)将获得锁;另一个(B)将在Lock
中的循环中旋转。
然后写到g_some_int
,然后调用Unlock
,它包含对clear
的调用,这是一个存储版本。在调用clear
之前对写入进行排序,因为它位于同一个线程中。
B然后在Lock
中唤醒,这一次test_and_set
调用返回false
。这是一个读取clear
调用存储的值的负载获取,因此对clear
的调用与对test_and_set
的调用同步。
test_and_set
调用在Lock
中是一个加载-获取,并在BlindWrite
中的g_some_int
写入之前进行排序,因为它位于同一个线程中。
由于线程A中的第一次写入是排序的--在调用clear
之前,这与线程B中对test_and_set
的调用同步,而后者又是顺序的--在线程B的写入之前,线程A中的写入发生在线程B的写入之前。
如果编译器将对g_some_int
的写挂在对Lock
的调用之上,那么线程B的写入就有可能发生在线程A中的写入之前。这将违反发生的情况--在排序之前,因此是不允许的。
一般来说,这意味着编译器不能在负载获取()之上提升任何,因为它可能会在排序之前违反发生的情况。
https://stackoverflow.com/questions/24901772
复制相似问题