为什么 std::condition_variable 的通知和等待函数都需要一个锁定的互斥体
Why do both the notify and wait function of a std::condition_variable need a locked mutex
在我对理解 std::contion_variable
的永无止境的探索中,我已经 运行 了解以下内容。在 this page 上,它表示如下:
void print_id (int id) {
std::unique_lock<std::mutex> lck(mtx);
while (!ready) cv.wait(lck);
// ...
std::cout << "thread " << id << '\n';
}
然后是这样写的:
void go() {
std::unique_lock<std::mutex> lck(mtx);
ready = true;
cv.notify_all();
}
据我了解,这两个函数都将在 std::unqique_lock
行停止。直到获得唯一锁。也就是说,没有其他线程有锁。
所以说 print_id
函数首先执行。将获取唯一锁,函数将在等待线上停止。
如果随后执行 go
函数(在单独的线程上),那里的代码将在唯一锁定行上停止。因为互斥锁已经被 print_id
函数锁定了。
显然,如果代码是这样的话,这是行不通的。但我真的不明白我在这里没有得到什么。所以请赐教。
您缺少的是 wait
解锁互斥锁,然后等待 cv
上的信号。
它在返回之前再次锁定互斥量。
您可以通过在找到示例的页面上单击 wait 来找到它:
At the moment of blocking the thread, the function automatically calls lck.unlock(), allowing other locked threads to continue.
Once notified (explicitly, by some other thread), the function unblocks and calls lck.lock(), leaving lck in the same state as when the function was called.
您错过了一点——调用 wait()
解锁 互斥锁。线程以原子方式(释放互斥量 + 进入睡眠状态)。然后,当被信号唤醒时,它会尝试重新获取互斥锁(可能是阻塞的);一旦获得它,它就可以继续。
请注意,调用 notify_*
时不需要锁定互斥量,仅 wait*
回答所提出的问题,这似乎是关于您不应该出于性能原因获取通知锁定的说法的必要条件(正确性不是比性能更重要吗?):锁定的必要性"wait" 并且始终锁定 "notify" 的建议是保护用户免受他自己和他的程序的数据和逻辑竞争的影响。如果不锁定 "go",您发布的程序将立即在 "ready" 上发生数据竞争。但是,即使 ready 本身是同步的(例如原子的),你也会有一个错过通知的逻辑竞争,因为没有 "go" 中的锁,通知可能会在 检查 "ready" 之后 和 实际等待之前 发生,然后等待线程可能会无限期地保持阻塞状态。原子变量本身的同步不足以防止这种情况发生。这就是为什么 helgrind 在没有持有锁的情况下完成通知时会发出警告的原因。有 一些 边缘情况,在通知周围确实不需要互斥锁。在所有这些情况下,都需要事先进行双向同步,以便生产线程可以确定另一个线程已经在等待。 IMO 这些案例仅供专家使用。实际上,我曾见过一位专家在谈论多线程时犯了这个错误——他认为原子计数器就足够了。也就是说,围绕等待的锁定对于正确性始终是必需的(或者至少是与等待原子操作),这就是标准库强制执行它并在进入等待时自动解锁互斥量的原因。
POSIX 条件变量与 Windows 事件不同,而不是 "idiot-proof" 因为它们是无状态的(除了知道等待线程)。在通知上使用锁的建议是为了保护您免受最糟糕和最常见的错误。当然,如果愿意,您可以使用互斥锁 + 条件变量 + 布尔变量构建类似 Windows 的有状态事件。
在我对理解 std::contion_variable
的永无止境的探索中,我已经 运行 了解以下内容。在 this page 上,它表示如下:
void print_id (int id) {
std::unique_lock<std::mutex> lck(mtx);
while (!ready) cv.wait(lck);
// ...
std::cout << "thread " << id << '\n';
}
然后是这样写的:
void go() {
std::unique_lock<std::mutex> lck(mtx);
ready = true;
cv.notify_all();
}
据我了解,这两个函数都将在 std::unqique_lock
行停止。直到获得唯一锁。也就是说,没有其他线程有锁。
所以说 print_id
函数首先执行。将获取唯一锁,函数将在等待线上停止。
如果随后执行 go
函数(在单独的线程上),那里的代码将在唯一锁定行上停止。因为互斥锁已经被 print_id
函数锁定了。
显然,如果代码是这样的话,这是行不通的。但我真的不明白我在这里没有得到什么。所以请赐教。
您缺少的是 wait
解锁互斥锁,然后等待 cv
上的信号。
它在返回之前再次锁定互斥量。
您可以通过在找到示例的页面上单击 wait 来找到它:
At the moment of blocking the thread, the function automatically calls lck.unlock(), allowing other locked threads to continue.
Once notified (explicitly, by some other thread), the function unblocks and calls lck.lock(), leaving lck in the same state as when the function was called.
您错过了一点——调用 wait()
解锁 互斥锁。线程以原子方式(释放互斥量 + 进入睡眠状态)。然后,当被信号唤醒时,它会尝试重新获取互斥锁(可能是阻塞的);一旦获得它,它就可以继续。
请注意,调用 notify_*
时不需要锁定互斥量,仅 wait*
回答所提出的问题,这似乎是关于您不应该出于性能原因获取通知锁定的说法的必要条件(正确性不是比性能更重要吗?):锁定的必要性"wait" 并且始终锁定 "notify" 的建议是保护用户免受他自己和他的程序的数据和逻辑竞争的影响。如果不锁定 "go",您发布的程序将立即在 "ready" 上发生数据竞争。但是,即使 ready 本身是同步的(例如原子的),你也会有一个错过通知的逻辑竞争,因为没有 "go" 中的锁,通知可能会在 检查 "ready" 之后 和 实际等待之前 发生,然后等待线程可能会无限期地保持阻塞状态。原子变量本身的同步不足以防止这种情况发生。这就是为什么 helgrind 在没有持有锁的情况下完成通知时会发出警告的原因。有 一些 边缘情况,在通知周围确实不需要互斥锁。在所有这些情况下,都需要事先进行双向同步,以便生产线程可以确定另一个线程已经在等待。 IMO 这些案例仅供专家使用。实际上,我曾见过一位专家在谈论多线程时犯了这个错误——他认为原子计数器就足够了。也就是说,围绕等待的锁定对于正确性始终是必需的(或者至少是与等待原子操作),这就是标准库强制执行它并在进入等待时自动解锁互斥量的原因。
POSIX 条件变量与 Windows 事件不同,而不是 "idiot-proof" 因为它们是无状态的(除了知道等待线程)。在通知上使用锁的建议是为了保护您免受最糟糕和最常见的错误。当然,如果愿意,您可以使用互斥锁 + 条件变量 + 布尔变量构建类似 Windows 的有状态事件。