为什么带有 std::memory_order_relaxed 的自旋锁可以正确执行?
why does spinlock with std::memory_order_relaxed perform correctly?
我用 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,宽松的顺序意味着 "there are no synchronization or ordering constraints",所以一个线程的更改并不意味着其他线程可以看到,对吗?为什么还是正确的?
(测试是在 Intel(R) Core(TM) i5-3337U CPU @ 1.80GHz 上 运行)
编辑:(感谢 Maxim 的评论)更新代码:初始化 SpinLock 中的数据成员,并更新测试代码。
我看到至少 x86-64 的 GCC 6.3 为 relaxed 和 release/acquire 生成了相同的代码。因此,结果相同也就不足为奇了。因此,要查看差异,您可能需要比 x86-64 提供的 TSO 更宽松的内存架构。大概,可能是ARM。
对互斥量实施使用宽松的排序约束会导致灾难。
根据定义,互斥锁旨在同步线程之间的数据。术语 acquire 和 release 与互斥锁密切相关;
您 获取 一个互斥锁,更改受其保护的数据并 释放 互斥锁,以便一旦另一个线程获取相同的互斥锁,数据就对它可见。
您引用的文章指出,对于宽松的操作,“没有同步或排序约束”...它适用于互斥体周围的内存操作,
不是互斥锁本身。通过宽松的排序,本应受互斥锁保护的数据实际上可能会被多个线程同时修改(引入数据竞争)。
在更强大的有序架构上,例如 X86
,它具有隐含的 acquire/release 语义,您将摆脱这种实现(因此您的测试成功)。
但是,运行 它在使用较弱内存排序的体系结构上,例如 Power
或 ARMv7
,你就有麻烦了。
您在评论中建议的顺序是正确的。
C++11 标准为原子操作指定了最弱的保证。并非所有硬件都能准确匹配每个最弱的保证,因此编译器和库编写者有时必须 "round up" 进行更强的操作。例如,x86 上的所有原子 read-modify-write 操作隐式具有 memory_order_acq_rel
.
另外,硬件架构的具体实现可能比硬件手册上说的更有保障。例如,早期的 Itaniums 实现了 memory_order_acq_rel 语义,甚至对于一些只承诺 memory_order_release.
的硬件指令也是如此
理论上,您的代码有可能在 x86 上失败,因为尊重原子操作的内存顺序涉及硬件和编译器。激进的编译器可以合法地将 'z' 的负载(也可能是存储!)移到仅使用 memory_order_release
排序的 tryLock
操作上。
我用 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,宽松的顺序意味着 "there are no synchronization or ordering constraints",所以一个线程的更改并不意味着其他线程可以看到,对吗?为什么还是正确的?
(测试是在 Intel(R) Core(TM) i5-3337U CPU @ 1.80GHz 上 运行)
编辑:(感谢 Maxim 的评论)更新代码:初始化 SpinLock 中的数据成员,并更新测试代码。
我看到至少 x86-64 的 GCC 6.3 为 relaxed 和 release/acquire 生成了相同的代码。因此,结果相同也就不足为奇了。因此,要查看差异,您可能需要比 x86-64 提供的 TSO 更宽松的内存架构。大概,可能是ARM。
对互斥量实施使用宽松的排序约束会导致灾难。
根据定义,互斥锁旨在同步线程之间的数据。术语 acquire 和 release 与互斥锁密切相关;
您 获取 一个互斥锁,更改受其保护的数据并 释放 互斥锁,以便一旦另一个线程获取相同的互斥锁,数据就对它可见。
您引用的文章指出,对于宽松的操作,“没有同步或排序约束”...它适用于互斥体周围的内存操作, 不是互斥锁本身。通过宽松的排序,本应受互斥锁保护的数据实际上可能会被多个线程同时修改(引入数据竞争)。
在更强大的有序架构上,例如 X86
,它具有隐含的 acquire/release 语义,您将摆脱这种实现(因此您的测试成功)。
但是,运行 它在使用较弱内存排序的体系结构上,例如 Power
或 ARMv7
,你就有麻烦了。
您在评论中建议的顺序是正确的。
C++11 标准为原子操作指定了最弱的保证。并非所有硬件都能准确匹配每个最弱的保证,因此编译器和库编写者有时必须 "round up" 进行更强的操作。例如,x86 上的所有原子 read-modify-write 操作隐式具有 memory_order_acq_rel
.
另外,硬件架构的具体实现可能比硬件手册上说的更有保障。例如,早期的 Itaniums 实现了 memory_order_acq_rel 语义,甚至对于一些只承诺 memory_order_release.
的硬件指令也是如此理论上,您的代码有可能在 x86 上失败,因为尊重原子操作的内存顺序涉及硬件和编译器。激进的编译器可以合法地将 'z' 的负载(也可能是存储!)移到仅使用 memory_order_release
排序的 tryLock
操作上。