为什么我们在进行条件变量通知之前需要一个空的std::lock_guard?
Why do we need an empty std::lock_guard before doing condition variable notify?
我目前正在研究Google的灯丝工作系统。你可以找到源代码here。让我感到困惑的部分是这个 requestExit() 方法:
void JobSystem::requestExit() noexcept {
mExitRequested.store(true);
{ std::lock_guard<Mutex> lock(mLooperLock); }
mLooperCondition.notify_all();
{ std::lock_guard<Mutex> lock(mWaiterLock); }
mWaiterCondition.notify_all();
}
我很困惑为什么我们需要锁定和解锁,即使在锁定和解锁之间没有任何动作。有没有必要这种空锁和解锁的情况?
这有点乱七八糟。首先,让我们看一下没有它的代码:
mExitRequested.store(true);
mLooperCondition.notify_all();
这里可能存在竞争条件。其他一些代码可能已经注意到 mExitRequested
是错误的,并在我们调用 notify_all
.
后立即开始等待 mLooperCondition
比赛将是:
- 其他线程检查
mExitRequested
,是false
。
- 我们将
mExitRequested
设为true
。
- 我们叫
mLooperCondition.notify_all
.
- 其他线程等待
mLooperCondition
。
- 糟糕。等待通知已经发生。
但是为了等待条件变量,您必须持有关联的互斥量。所以这只有在其他线程持有 mLooperLock
互斥锁时才会发生。事实上,第 4 步实际上是:“其他线程释放 mLooperLock
并等待 mLooperCondition
。
所以,要让这场比赛发生,它必须完全像这样发生:
- 其他线程获取
mLooperLock
。
- 其他线程检查
mExitRequested
,是false
。
- 我们将
mExitRequested
设为true
。
- 我们叫
mLooperCondition.notify_all
.
- 其他线程等待
mLooperCondition
,释放mLooperLock
。
- 糟糕。等待通知已经发生。
因此,如果我们将代码更改为:
mExitRequested.store(true);
{ std::lock_guard<Mutex> lock(mLooperLock); }
mLooperCondition.notify_all();
这确保没有其他线程可以检查 mExitRequested
并查看 false
然后等待 mLooperCondition
。因为另一个线程必须在整个进程中持有 mLooperLock
锁,这是不可能发生的,因为我们是在该进程的中间获取它的。
再试一次:
- 其他线程获取
mLooperLock
。
- 其他线程检查
mExitRequested
,是false
。
- 我们将
mExitRequested
设为true
。
- 通过获取和释放
nLooperLock
,在另一个线程释放mLooperLock
之前,我们不会取得任何进展。
- 我们叫
mLooperCondition.notify_all
.
现在,其他线程要么阻塞条件,要么不阻塞。如果没有,则没有问题。如果是,也没有问题,因为 mLooperLock
的解锁是条件变量的原子 "unlock and wait" 操作,保证它看到我们的通知。
我目前正在研究Google的灯丝工作系统。你可以找到源代码here。让我感到困惑的部分是这个 requestExit() 方法:
void JobSystem::requestExit() noexcept {
mExitRequested.store(true);
{ std::lock_guard<Mutex> lock(mLooperLock); }
mLooperCondition.notify_all();
{ std::lock_guard<Mutex> lock(mWaiterLock); }
mWaiterCondition.notify_all();
}
我很困惑为什么我们需要锁定和解锁,即使在锁定和解锁之间没有任何动作。有没有必要这种空锁和解锁的情况?
这有点乱七八糟。首先,让我们看一下没有它的代码:
mExitRequested.store(true);
mLooperCondition.notify_all();
这里可能存在竞争条件。其他一些代码可能已经注意到 mExitRequested
是错误的,并在我们调用 notify_all
.
mLooperCondition
比赛将是:
- 其他线程检查
mExitRequested
,是false
。 - 我们将
mExitRequested
设为true
。 - 我们叫
mLooperCondition.notify_all
. - 其他线程等待
mLooperCondition
。 - 糟糕。等待通知已经发生。
但是为了等待条件变量,您必须持有关联的互斥量。所以这只有在其他线程持有 mLooperLock
互斥锁时才会发生。事实上,第 4 步实际上是:“其他线程释放 mLooperLock
并等待 mLooperCondition
。
所以,要让这场比赛发生,它必须完全像这样发生:
- 其他线程获取
mLooperLock
。 - 其他线程检查
mExitRequested
,是false
。 - 我们将
mExitRequested
设为true
。 - 我们叫
mLooperCondition.notify_all
. - 其他线程等待
mLooperCondition
,释放mLooperLock
。 - 糟糕。等待通知已经发生。
因此,如果我们将代码更改为:
mExitRequested.store(true);
{ std::lock_guard<Mutex> lock(mLooperLock); }
mLooperCondition.notify_all();
这确保没有其他线程可以检查 mExitRequested
并查看 false
然后等待 mLooperCondition
。因为另一个线程必须在整个进程中持有 mLooperLock
锁,这是不可能发生的,因为我们是在该进程的中间获取它的。
再试一次:
- 其他线程获取
mLooperLock
。 - 其他线程检查
mExitRequested
,是false
。 - 我们将
mExitRequested
设为true
。 - 通过获取和释放
nLooperLock
,在另一个线程释放mLooperLock
之前,我们不会取得任何进展。 - 我们叫
mLooperCondition.notify_all
.
现在,其他线程要么阻塞条件,要么不阻塞。如果没有,则没有问题。如果是,也没有问题,因为 mLooperLock
的解锁是条件变量的原子 "unlock and wait" 操作,保证它看到我们的通知。