考虑以下自旋锁的实现(Google 中关于查询“c++ 自旋锁实现”的第一个链接):
struct spinlock {
std::atomic<bool> lock_ = {0};
void lock() noexcept {
for (;;) {
if (!lock_.exchange(true, std::memory_order_acquire)) {
return;
}
while (lock_.load(std::memory_order_relaxed)) { // what's the guarantee????
asm volatile ("yield\nyield\nyield");
}
}
}
bool try_lock() noexcept {
return !lock_.load(std::memory_order_relaxed) && // why not acquire???
!lock_.exchange(true, std::memory_order_acquire);
}
void unlock() noexcept {
lock_.store(false, std::memory_order_release);
}
};
但是,对我来说这看起来不正确:在 Arm64 上,
memory_order_relaxed
读取不能保证刷新失效队列(与带有 TSO 的 X86 不同)。是bug还是我错了?
一种常见的误解是,宽松的内存排序不足以确保原子写入完全可见,或者原子读取最终观察到它们。确实,非原子写入和读取存在这个问题,因为编译器可能会完全优化它们。但对于原子操作,我们有 [intro.progress p18](使用 C++20):
实现应确保由原子或原子分配的最后一个值(按修改顺序) 同步操作将在有限的时间内对所有其他线程可见。
(人们有时会争论这是“应该”而不是“必须”,但事实是,任何不遵守这一点的实现都将无法使用。我的感觉是,他们只是因为声明而使用“应该”规则在形式上不如记忆模型的其余部分严格,他们不希望任何人强迫他们提供精确的数学公式。)
因此,当另一个线程释放锁时,最终循环中的松弛负载必须观察它。典型的机器将确保这种情况发生,不仅是在有限的时间内,而且实际上没有任何不必要的延迟。
ARM 架构也在机器级别承诺:如果对某个内存位置进行存储,则从该位置加载最终会观察到存储的值,即使不使用额外的内存屏障。这至少适用于同一共享域中的观察者,但同一进程的两个线程将始终在同一内部共享域中的核心上运行,并且它们之间共享的内存将始终是正常内部共享。
在 ARMv8 规范中很难找到此属性的精确声明,但我们在 B2.7.1 中确实有“对具有 Normal 属性的内存位置的写入在有限时间内完成”,并进一步向下,“每个内部可共享性域包含一组观察者,该观察者组中的每个成员的数据是一致的 由该集合的任何成员进行的具有内部可共享属性的数据访问。”。不幸的是,他们似乎没有给出“数据一致”的精确定义,但它肯定必须包括“加载最终观察存储”。