std::mutex 是不是顺序一致?

Posted

技术标签:

【中文标题】std::mutex 是不是顺序一致?【英文标题】:Is std::mutex sequentially consistent?std::mutex 是否顺序一致? 【发布时间】:2017-01-25 09:12:37 【问题描述】:

说,我有两个线程 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> lockmA;
            fA = true;
         ;
    thread B [] 
            lock_guard<mutex> lockmB;
            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> lockcoutMutex;
            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> lockcoutMutex;
            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&lt;bool&gt; 变量使用顺序一致的内存顺序或获取/释放内存顺序来实现自旋锁。所以问题是std::mutex 的行为是否类似于顺序一致的自旋锁或获取/释放内存顺序自旋锁。

【问题讨论】:

编辑了我的答案(不确定你是否收到通知),现在它几乎与以前相反,std::mutex is 就像一个获取/释放自旋锁,但也你给出的输出是不可能的 【参考方案1】:

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

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

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

11 同步:对同一对象的先前解锁()操作应 与 (1.10) 这个操作同步 [lock()].

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

大编辑:

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

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

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

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

【讨论】:

在实践中,POSIX 互斥锁似乎不是顺序一致的。我已经检查了 glibc 的实现,他们正在使用“获取”发布顺序。 Afaik musl 做同样的事情。 没错,afaik ARM64 是一样的(版本是 seq_cst)。但是 32 位 ARM 是不同的,这可能仍然与某些人有关。 @curiousguy 再看一遍,我认为没有。每个互斥体都有一个定义的锁定/解锁序列,因此给定的序列不可能发生。我研究了 c++ 内存模型的细节,虽然我没有足够的信心在这种情况下做出合乎逻辑的证明,但我很确定你可以。此外,x86/64 的发布不是 seq_cst,我记错了所有写入都是发布,所有读取都是获取。缺少 seq_cst 只是意味着无法确定互斥量 A 或互斥量 B 是否首先被锁定,但这不会影响此程序中的输出 @JosephIreland 这些原子“配方”(“C/C++11 mappings to processors”)和许多其他资源将 Intel x86 上的所有存储描述为释放存储:“存储释放”映射到“MOV(进入内存)”并且释放围栏是 NOP! @curiousguy 我同意这是一种干扰,因为它不会影响该程序的输出。我对这个答案做了很大的修改。【参考方案2】:

是否可以在不同的情况下观察到 fA 和 fB 的修改? 不同线程 C 和 D 中的订单?

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

【讨论】:

问题是对不同的变量fAfB 使用2 个不同的锁。所以有 2 个独立的历史,它们之间的交错方式没有任何限制。问题是不能同时观察 2 个不同的 reader,因为 reader 还需要获取相应的锁。 @PeterCordes 假设是对共享对象 X 的任何访问都是在 lock(M_X) 之后完成的。这些不同的、不完整的历史在 X 上是本地的。任何想要访问另一个共享对象的线程都将获得一个锁并“拉”另一个本地历史,从而创建一个全局历史。 好的,是的,这似乎是您的答案中缺少的解释。顺便说一句,您可以在互斥锁内使用atomic_intmo_relaxed,然后可以让两个阅读器以相反的顺序阅读mo_acquire,从而允许两个阅读实际上同时发生。我认为 ISO C++ 会允许读者在编写顺序上存在分歧(IRIW 案例;在 POWER 的实践中可能。我认为 POWER 仅在 seq_cst 加载时避免它,而不是在商店中设置强大的障碍,所以我不认为标准库互斥锁会阻止它。) 嗯,再三考虑阅读 mo_acquire 并不能证明什么。如果作者使用mo_seq_cst 原子存储而不是mo_relaxed 周围的互斥体,则仍然允许IRIW 重新排序(并且在实践中可能在POWER 上)。所以是的,读取所需的锁定是这里的关键,防止两个读取命令同时发生。

以上是关于std::mutex 是不是顺序一致?的主要内容,如果未能解决你的问题,请参考以下文章

互斥锁的发生顺序是不是与询问的顺序相同?

死锁使用 std::mutex 保护多线程中的 cout

std::shared_mutex 是不是偏爱作者而不是读者?

领域查询顺序是不是一致?

Java HashMap keySet() 迭代顺序是不是一致?

是否有任何理由应该将 C++ 11+ std::mutex 声明为全局变量,而不是作为函数参数传递给 std::thread ?