std::mutex顺序一致吗?
Is std::mutex sequentially consistent?
说,我有两个线程 A
和 B
分别写入全局布尔变量 fA
和 fB
,它们最初设置为 false
并分别受到 std::mutex
个对象 mA
和 mB
的保护:
// Thread A
mA.lock();
assert( fA == false );
fA = true;
mA.unlock();
// Thread B
mB.lock()
assert( fB == false );
fB = true;
mB.unlock()
是否可以在不同的线程C
和D
中以不同的顺序观察fA
和fB
的修改?也就是说,下面的程序可以
#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" 与执行解锁的线程所看到的所有过去的修改。所以只有一个历史记录,不仅是锁定-解锁操作,还有对共享对象的访问,可以存在。
说,我有两个线程 A
和 B
分别写入全局布尔变量 fA
和 fB
,它们最初设置为 false
并分别受到 std::mutex
个对象 mA
和 mB
的保护:
// Thread A
mA.lock();
assert( fA == false );
fA = true;
mA.unlock();
// Thread B
mB.lock()
assert( fB == false );
fB = true;
mB.unlock()
是否可以在不同的线程C
和D
中以不同的顺序观察fA
和fB
的修改?也就是说,下面的程序可以
#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" 与执行解锁的线程所看到的所有过去的修改。所以只有一个历史记录,不仅是锁定-解锁操作,还有对共享对象的访问,可以存在。