对于 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 属性的内存位置的写入在有限时间内完成”,并进一步向下,“每个内部可共享性域包含一组观察者,该观察者组中的每个成员的数据是一致的 由该集合的任何成员进行的具有内部可共享属性的数据访问。”。不幸的是,他们似乎没有给出“数据一致”的精确定义,但它肯定必须包括“加载最终观察存储”。

关于您最近参与的评论线程中引用的paper:即使宽松的负载不会导致无效队列立即刷新,这并不意味着无效消息将仅位于该队列中无限期地排队。核心总是会尽快处理这些消息,因此最终(通常很快),缓存行无论如何都会失效。此时,后续加载将必须从修改的核心请求行,然后将观察到新值。

同样,在编写者方面,即使存储确实被放入存储缓冲区中而不是立即提交,它也不会只是坐在那里;而是会继续存在。核心会尽快处理这些商店,因此它将在不久的将来在全球范围内可见,而不需要任何障碍或其他进一步的指令。

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