Arm64 的 TTAS 自旋锁中的 `memory_order_relaxed` 是否足够?

问题描述 投票:0回答:1

考虑以下自旋锁的实现(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还是我错了?

c++ arm memory-barriers spinlock mesi
1个回答
0
投票

一种常见的误解是,宽松的内存排序不足以确保原子写入完全可见,或者原子读取最终观察到它们。确实,非原子写入和读取存在这个问题,因为编译器可能会完全优化它们。但对于原子操作,我们有 [intro.progress p18](使用 C++20):

实现应确保由原子或原子分配的最后一个值(按修改顺序) 同步操作将在有限的时间内对所有其他线程可见。

(人们有时会争论这是“应该”而不是“必须”,但事实是,任何不遵守这一点的实现都将无法使用。我的感觉是,他们只是因为声明而使用“应该”规则在形式上不如记忆模型的其余部分严格,他们不希望任何人强迫他们提供精确的数学公式。)

因此,当另一个线程释放锁时,最终循环中的松弛负载必须观察它。典型的机器将确保这种情况发生,不仅是在有限的时间内,而且实际上没有任何不必要的延迟。

ARM 架构也在机器级别承诺:如果对某个内存位置进行存储,则从该位置加载最终会观察到存储的值,即使不使用额外的内存屏障。这至少适用于同一共享域中的观察者,但同一进程的两个线程将始终在同一内部共享域中的核心上运行,并且它们之间共享的内存将始终是正常内部共享。

在 ARMv8 规范中很难找到此属性的精确声明,但我们在 B2.7.1 中确实有“对具有 Normal 属性的内存位置的写入在有限时间内完成”,并进一步向下,“每个内部可共享性域包含一组观察者,该观察者组中的每个成员的数据是一致的 由该集合的任何成员进行的具有内部可共享属性的数据访问。”。不幸的是,他们似乎没有给出“数据一致”的精确定义,但它肯定必须包括“加载最终观察存储”。

© www.soinside.com 2019 - 2024. All rights reserved.