我使用C ++ 11原子库实现了一个自旋锁:
class SpinLock {
atomic_bool latch_;
public:
SpinLock() :latch_(false){
}
void lock() {
while(tryLock() == false);
}
bool tryLock() {
bool b = false;
return latch_.compare_exchange_weak(b,true,std::memory_order_relaxed);
}
void unlock() {
latch_.store(false,std::memory_order_relaxed);
}
};
我通过生成多个线程测试了正确性,如下所示:
static int z = 0;
static SpinLock spinLock;
static void safeIncrement(int run) {
while(--run >= 0) {
std::lock_guard<SpinLock> guard(spinLock);
++z;
}
}
static void test(int nThreads =2) {
std::vector<std::thread*> workers(nThreads);
z = 0;
for(auto& ptr : workers) ptr = new std::thread(safeIncrement,1<<20);
for(auto ptr : workers) ptr->join();
cout<<"after increment: " <<z << " out of " << (1<<20) * nThreads<<endl;
for(auto ptr : workers) delete ptr;
}
int main() {
test(4);
return 0;
}
我很惊讶最后的总数加起来是一个正确的值,轻松的顺序。通过这篇文章:http://en.cppreference.com/w/cpp/atomic/memory_order,宽松的顺序意味着“没有同步或排序约束”,所以从一个线程的变化并不意味着从其他人看到,对吧?为什么它仍然正确?
(该测试在Intel(R)Core(TM)i5-3337U CPU @ 1.80GHz上运行)
编辑:(感谢Maxim的评论)更新了代码:在SpinLock中初始化数据成员,并更新测试代码。
答案 0 :(得分:1)
我看到至少G86 6.3在x86-64上为轻松和发布/获取生成了相同的代码。因此,结果是相同的并不奇怪。因此,要看到差异,您可能需要比x86-64提供的TSO更宽松的内存架构。可能是它可能是ARM。
答案 1 :(得分:1)
C ++ 11标准规定了原子操作最薄弱的保证。并非所有硬件都能完全匹配每个最薄弱的保证,因此编译器和库编写者有时必须&#34;向上舍入&#34;加强运营。例如,x86上的所有原子读取 - 修改 - 写入操作都隐式具有memory_order_acq_rel
。
此外,硬件架构的特定实现可能比硬件手册所说的更强。例如,早期的Itaniums实现了memory_order_acq_rel语义,即使对于只承诺memory_order_release的一些硬件指令也是如此。
理论上,您的代码可能在x86上失败,因为尊重原子操作的内存排序涉及两者硬件和编译器。积极的编译器可以合法地移动&#39; z&#39; (也可能是商店!)仅仅使用tryLock
订购的memory_order_release
操作。
答案 2 :(得分:0)
对互斥锁实现使用宽松的排序约束是灾难的一个方法 根据定义,互斥锁用于在线程之间同步数据。术语获取和发布与互斥锁密切相关; 您获取互斥锁,更改受其保护的数据并释放互斥锁,以便在获取相同的互斥锁后,数据对另一个线程可见。
您所指的文章指出,对于宽松操作,“没有同步或排序约束”...它适用于互斥锁周围的内存操作, 不是互斥体本身。通过宽松的排序,应该由互斥锁保护的数据实际上可以由多个线程同时修改(引入数据竞争)。
在一个更强大的有序体系结构(例如X86
,具有隐式获取/释放语义)上,您将完成此实现(因此您的测试成功)。
但是,在使用较弱内存排序的架构上运行它,例如Power
或ARMv7
,您就遇到了麻烦。
您在评论中建议的顺序是正确的。