std::mutex顺序一致吗?

Is std::mutex sequentially consistent?

说,我有两个线程 AB 分别写入全局布尔变量 fAfB,它们最初设置为 false并分别受到 std::mutex 个对象 mAmB 的保护:

// Thread A
mA.lock();
assert( fA == false );
fA = true;
mA.unlock();

// Thread B
mB.lock()
assert( fB == false );
fB = true;
mB.unlock()

是否可以在不同的线程CD中以不同的顺序观察fAfB的修改?也就是说,下面的程序可以

#include <atomic>
#include <cassert>
#include <iostream>
#include <mutex>
#include <thread>
using namespace std;

mutex mA, mB, coutMutex;
bool fA = false, fB = false;

int main()
{
    thread A{ []{
            lock_guard<mutex> lock{mA};
            fA = true;
        } };
    thread B{ [] {
            lock_guard<mutex> lock{mB};
            fB = true;
        } };
    thread C{ [] { // reads fA, then fB
            mA.lock();
            const auto _1 = fA;
            mA.unlock();
            mB.lock();
            const auto _2 = fB;
            mB.unlock();
            lock_guard<mutex> lock{coutMutex};
            cout << "Thread C: fA = " << _1 << ", fB = " << _2 << endl;
        } };
    thread D{ [] { // reads fB, then fA (i. e. vice versa)
            mB.lock();
            const auto _3 = fB;
            mB.unlock();
            mA.lock();
            const auto _4 = fA;
            mA.unlock();
            lock_guard<mutex> lock{coutMutex};
            cout << "Thread D: fA = " << _4 << ", fB = " << _3 << endl;
        } };
    A.join(); B.join(); C.join(); D.join();
}

合法印刷

Thread C: fA = 1, fB = 0
Thread D: fA = 0, fB = 1

根据C++标准?

注意: 自旋锁可以使用 std::atomic<bool> 变量使用顺序一致的内存顺序或 acquire/release 内存顺序来实现。所以问题是 std::mutex 的行为是否像顺序一致的自旋锁或 acquire/release 内存顺序自旋锁。

是的,那是允许的那个输出是不可能的,但是std::mutex不一定是顺序一致的。 Acquire/release 足以排除这种行为。

std::mutex在标准中没有定义顺序一致,只是说

30.4.1.2 Mutex types [thread.mutex.requirements.mutex]

11 Synchronization: Prior unlock() operations on the same object shall synchronize with (1.10) this operation [lock()].

Synchronize-with 似乎与 std::memory_order::release/acquire 的定义相同(参见 this question)。
据我所知,acquire/release 自旋锁将满足 std::mutex.

的标准

大编辑:

但是,我不认为这意味着您的想法(或我的想法)。输出仍然是不可能的,因为 acquire/release 语义足以排除它。这是一种更好解释的微妙点here。起初这似乎显然是不可能的,但我认为对这样的事情保持谨慎是正确的。

根据标准,unlock() lock() 同步。这意味着 unlock() 之前发生的任何事情在 lock() 之后都是可见的。 Happens before(此后 ->)是一个有点奇怪的关系,在上面 link 中有更好的解释,但是因为在这个例子中所有的东西都有互斥锁,所以一切都像你期望的那样工作,即const auto _1 = fA; 发生在 const auto _2 = fB; 之前,并且当它 unlock() 互斥锁时线程可见的任何更改对 [=16] 的下一个线程可见=]s 互斥量。它还具有一些预期的属性,例如如果 X 发生在 Y 之前,Y 发生在 Z 之前,则 X -> Z,如果 X 发生在 Y 之前,则 Y 不会发生在 X 之前。

从这里不难看出直觉上似乎正确的矛盾。

简而言之,每个互斥体都有明确定义的操作顺序 - 例如对于互斥体 A,线程 A、C、D 以某种顺序持有锁。对于线程 D 要打印 fA=0,它必须在线程 A 之前锁定 mA,对于线程 C 反之亦然。所以 mA 的锁定顺序是 D(mA) -> A(mA) -> C(mA)。

对于互斥锁 B,序列必须是 C(mB) -> B(mB) -> D(mB)。

但是从程序中我们知道 C(mA) -> C(mB),所以让我们把两者放在一起得到 D(mA) -> A(mA) -> C(mA) -> C (mB) -> B(mB) -> D(mB),即 D(mA) -> D(mB)。但是代码也给了我们 D(mB) -> D(mA),这是矛盾的,这意味着您观察到的输出是不可能的。

这个结果对于 acquire/release 自旋锁没有什么不同,我认为每个人都混淆了对变量的常规 acquire/release 内存访问与对受自旋锁保护的变量的访问。不同之处在于,使用自旋锁,读取线程还执行 compare/exchange 和释放写入,这与单个释放写入和获取读取是完全不同的场景。

如果您使用顺序一致的自旋锁,那么这不会影响输出。唯一的区别是您始终可以从未获取任何锁的单独线程中明确回答 "mutex A was locked before mutex B" 之类的问题。但是对于这个例子和大多数其他例子,这种声明是没有用的,因此 acquire/release 是标准。

Is it possible to observe the modifications on fA and fB in different orders in different threads C and D?

锁的基本思想 "acquiring" 解锁的 "released" 状态(和副作用历史)使这成为不可能:您承诺仅通过获取相应的锁来访问共享对象,并且该锁将 "synchronize" 与执行解锁的线程所看到的所有过去的修改。所以只有一个历史记录,不仅是锁定-解锁操作,还有对共享对象的访问,可以存在。