为什么我们在进行条件变量通知之前需要一个空的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

比赛将是:

  1. 其他线程检查mExitRequested,是false
  2. 我们将mExitRequested设为true
  3. 我们叫mLooperCondition.notify_all.
  4. 其他线程等待 mLooperCondition
  5. 糟糕。等待通知已经发生。

但是为了等待条件变量,您必须持有关联的互斥量。所以这只有在其他线程持有 mLooperLock 互斥锁时才会发生。事实上,第 4 步实际上是:“其他线程释放 mLooperLock 并等待 mLooperCondition

所以,要让这场比赛发生,它必须完全像这样发生:

  1. 其他线程获取 mLooperLock
  2. 其他线程检查mExitRequested,是false
  3. 我们将mExitRequested设为true
  4. 我们叫mLooperCondition.notify_all.
  5. 其他线程等待mLooperCondition,释放mLooperLock
  6. 糟糕。等待通知已经发生。

因此,如果我们将代码更改为:

mExitRequested.store(true);
{ std::lock_guard<Mutex> lock(mLooperLock); }
mLooperCondition.notify_all();

这确保没有其他线程可以检查 mExitRequested 并查看 false 然后等待 mLooperCondition。因为另一个线程必须在整个进程中持有 mLooperLock 锁,这是不可能发生的,因为我们是在该进程的中间获取它的。

再试一次:

  1. 其他线程获取 mLooperLock
  2. 其他线程检查mExitRequested,是false
  3. 我们将mExitRequested设为true
  4. 通过获取和释放nLooperLock,在另一个线程释放mLooperLock之前,我们不会取得任何进展。
  5. 我们叫mLooperCondition.notify_all.

现在,其他线程要么阻塞条件,要么不阻塞。如果没有,则没有问题。如果是,也没有问题,因为 mLooperLock 的解锁是条件变量的原子 "unlock and wait" 操作,保证它看到我们的通知。