繁体   English   中英

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>变量使用顺序一致的内存顺序或获取/释放内存顺序来实现自旋锁。 所以问题是std::mutex行为是否像顺序一致的自旋锁或获取/释放内存顺序自旋锁。

是的,这是允许的 输出是不可能的,但std::mutex不一定顺序一致。 获取/释放足以排除这种行为。

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

30.4.1.2 互斥类型 [thread.mutex.requirements.mutex]

11 同步:对同一对象的先前的 unlock() 操作应与 (1.10) 此操作[lock()]同步。

Synchronize-with似乎与std::memory_order::release/acquire定义相同(请参阅此问题)。
据我所知,获取/释放自旋锁将满足 std::mutex 的标准。

大编辑:

但是,我认为这并不意味着您的想法(或我的想法)。 输出仍然是不可能的,因为获取/释放语义足以排除它。 这是一种微妙的点, 在这里更好地解释。 一开始这显然是不可能的,但我认为对这样的事情保持谨慎是正确的。

根据标准,unlock()lock()同步 这意味着unlock()之前发生的任何事情在lock() 之后都是可见的。 之前发生的(从今以后 ->)是一个稍微奇怪的关系,在上面的链接中得到了更好的解释,但是因为在这个例子中所有东西都有互斥锁,所以一切都像你期望的那样工作,即const auto _1 = fA; 发生在const auto _2 = fB; ,以及当一个线程在unlock() s 互斥锁时可见的任何更改对lock() s 互斥lock()的下一个线程可见。 它还有一些预期的属性,例如,如果 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),这是一个矛盾,意味着你观察到的输出是不可能的。

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

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

是否可以在不同的线程 C 和 D 中以不同的顺序观察对 fA 和 fB 的修改?

锁“获取”解锁的“释放”状态(和副作用历史)的基本思想使这成为不可能:您承诺仅通过获取相应的锁来访问共享对象,并且该锁将与所有“同步”执行解锁的线程看到的过去修改。 因此,只能存在一种历史,不仅是锁定-解锁操作,而且是对共享对象的访问。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM