同一互斥体上的锁定和解锁顺序是否一致?

Are lock and unlock on the same mutex sequential consistent?

对于互斥lock(),标准mentions:

Prior unlock() operations on the same mutex synchronize-with (as defined in std::memory_order) this operation.

这个answer试图按照标准来解释synchronize-with是什么意思。不过,貌似定义没有明确说明。

我的主要问题是,我能得到这个输出吗:

x: 1
y: 2

由于线程 A 中的内存重新排序,以下代码?如果 A 解锁后 B 锁定,A 中的 x 上的写入是否保证被 B 观察到?

std::mutex mutex;
int x = 0, y = 0;

int main() {
  std::thread A{[] {
    x = 1;
    std::lock_guard<std::mutex> lg(std::mutex);
    y = 0;
  }};
  std::thread B{[] {
    std::lock_guard<std::mutex> lg(std::mutex);
    y = x + 2;
  }};

  A.join();
  B.join();
  std::cout << "x: " << x << std::endl;
  std::cout << "y: " << y << std::endl;
}

如果不是,基于标准的哪一部分? 换句话说,我们可以假设 lock/unlock?

之间存在顺序一致性吗?

我也看到了这个 问题,但它是针对单独的互斥体的。

明确定义了同步关系。该标准规定如下:

Certain library calls synchronize with other library calls performed by another thread. For example, an atomic store-release synchronizes with a load-acquire that takes its value from the store. [...] [ Note: The specifications of the synchronization operations define when one reads the value written by another. For atomic objects, the definition is clear. All operations on a given mutex occur in a single total order. Each mutex acquisition "reads the value written" by the last mutex release. — end note ]

还有:

An atomic operation A that performs a release operation on an atomic object M synchronizes with an atomic operation B that performs an acquire operation on M and takes its value from any side effect in the release sequence headed by A.

所以换句话说,如果一个acquire操作A "sees"一个release操作存储的值B,那么AB.

同步

考虑一个自旋锁,您只需要一个原子 bool 标志。所有操作都在该标志上进行。为了获得锁,您已经使用原子读-修改-写操作设置了标志。原子对象上的所有修改完全按修改顺序排序,并且保证 RMW 操作始终读取在与该 RMW 操作关联的写入之前写入的最后一个值(按修改顺序)。

由于这种保证,lock/unlock 操作使用 acquire/release 语义就足够了,因为成功的锁定操作总是 "sees" 前一个解锁写入的值。

关于您的问题:

Is the write on x in A guaranteed to be observed by B if B locks after A unlocks?

重要的部分是"if B locks after A unlocks"!如果这是有保证的,那么是的,B 的锁定操作与 A 的解锁同步,从而建立了一个 happens-before 关系。因此 B 将观察 A 的写入。但是,您的代码不提供 BA 之后锁定的保证,因此您有潜在的数据竞争,这将导致@ReinstateMonica 正确指出的未定义行为。

更新
对 x 的写入在 A 的解锁之前排序。操作是否在互斥量之外(之前)并不重要。事实上,从理论上讲,编译器可以重新排序操作,以便它最终在 inside 互斥锁中结束(尽管这不太可能)。 Sequenced-before 也是 happens-before 定义的一部分,所以我们有以下内容:

std::thread A{[] {
    x = 1; // a
    std::lock_guard<std::mutex> lg(std::mutex);
    y = 0;
    // implicit unlock: b
  }};
  std::thread B{[] {
    std::lock_guard<std::mutex> lg(std::mutex); // c
    y = x + 2;
  }};

假设 BA 解锁后锁定,我们有:

  • ab 之前排序 -> ab
  • bc 同步 -> b 发生在 [=51= 之前]c

并且由于先行关系是传递性的,因此 a 先行发生 c。所以是的,对于 A 解锁之前排序的 所有 操作都是如此 - 无论它们是否在锁内。