在通知条件变量后使用互斥锁
Use of mutex after condition variable has been notified
通知的条件变量在通知后重新锁定互斥锁是什么原因
如果 unique_lock 没有作用域或者互斥量没有明确解锁,下面的代码会死锁
#include <future>
#include <mutex>
#include <iostream>
using namespace std;
int main()
{
std::mutex mtx;
std::condition_variable cv;
//simulate another working thread sending notification
auto as = std::async([&cv](){ std::this_thread::sleep_for(std::chrono::seconds(2));
cv.notify_all();});
//uncomment scoping (or unlock below) to prevent deadlock
//{
std::unique_lock<std::mutex> lk(mtx);
//Spurious Wake-Up Prevention not adressed in this short sample
//UNLESS it is part of the answer / reason to lock again
cv.wait(lk);
//}
std::cout << "CV notified\n" << std::flush;
//uncomment unlock (or scoping above) to prevent deadlock
//mtx.unlock();
mtx.lock();
//do something
mtx.unlock();
std::cout << "End may never be reached\n" << std::flush;
return 0;
}
即使重新阅读 some documentation and examples 我仍然不觉得这很明显。
可以在网上找到的大多数示例都是小型代码示例,它们具有 unique_lock.
的固有范围
我们是否应该使用不同的互斥锁来处理临界区(互斥锁 1)和条件变量等待和通知(互斥锁 2)?
注意:调试显示等待阶段结束后,"internal" "mutex count"(我认为结构 __pthread_mutex_s 的字段 __count )从 1 变为 2 .解锁后回到0
您正在尝试锁定互斥体两次。一次使用 unique_lock 并再次使用显式 mutex.lock()
调用。对于非递归互斥锁,它会在重新锁定尝试时死锁,让您知道您有错误。
std::unique_lock<std::mutex> lk(mtx); // This locks for the lifetime of the unique_lock object
cv.wait(lk); // this will unlock while waiting, but relock on return
std::cout << "CV notified\n" << std::flush;
mtx.lock(); // This attempts to lock the mutex again, but will deadlock since unique_lock has already invoked mutex.lock() in its constructor.
修复非常接近那些未注释的花括号。只需确保在互斥锁上一次只有一个锁处于活动状态。
此外,如您所见,您的代码很容易被虚假唤醒。这里有一些调整给你。您应该始终停留在等待循环中,直到条件或状态(通常由互斥锁本身保护)实际发生。对于简单的通知,bool 就可以了。
int main()
{
std::mutex mtx;
std::condition_variable cv;
bool conditon = false;
//simulate another working thread sending notification
auto as = std::async([&cv, &mtx, &condition](){
std::this_thread::sleep_for(std::chrono::seconds(2));
mtx.lock();
condition = true;
mtx.unlock();
cv.notify_all();});
std::unique_lock<std::mutex> lk(mtx); // acquire the mutex lock
while (!condition)
{
cv.wait(lk);
}
std::cout << "CV notified\n" << std::flush;
//do something - while still under the lock
return 0;
}
因为条件等待可能 return 除了被通知之外的原因,例如信号,或者只是因为其他人写入了相同的 64 字节缓存行。或者它可能已收到通知但条件不再为真,因为另一个线程处理了它。
所以互斥锁被锁定,这样您的代码就可以在持有互斥锁的同时检查其条件变量。也许这只是一个布尔值,表示它已准备就绪。
不要跳过那部分。如果你这样做,你会后悔的。
让我们暂时假设 mutex
不是 从 wait
:
锁定在 return
线程 1:
锁定 mutex
,检查谓词(无论可能是什么),并在发现谓词不是可接受的形式时,等待其他线程将其放入可接受的形式。等待 原子地 将线程 1 置于睡眠状态并解锁 mutex
。 mutex
解锁后,其他一些线程将有权将谓词置于可接受的状态(谓词自然不是线程安全的)。
线程 2:
同时,此线程正在尝试锁定 mutex
并将谓词置于线程 1 可接受的状态以继续等待。它必须在 mutex
锁定的情况下执行此操作。 mutex
保护谓词不被多个线程同时访问(读取或写入)。
一旦线程 2 将 mutex
置于可接受状态,它会通知 condition_variable
并解锁 mutex
(这两个动作的顺序与此参数无关) .
线程 1:
现在线程 1 已收到通知,我们假设 mutex
未从 wait
锁定到 return。线程 1 必须做的第一件事是检查谓词以查看它是否真的可以接受(这可能是虚假唤醒)。但是它不应该在 mutex
没有被锁定的情况下检查谓词。否则其他线程可能会在该线程检查谓词后立即更改谓词,从而使该检查的结果无效。
所以非常这个线程在醒来时必须做的第一件事是锁定mutex
,然后检查谓词。
所以mutex
从wait
锁定在return确实更方便。否则,等待线程必须在 100% 的时间内手动锁定它。
让我们再看看当线程 1 进入 wait
时的事件:我说过睡眠和解锁 自动发生 。这个非常重要。想象一下,如果线程 1 必须手动解锁 mutex
和 ,然后 调用 wait
:在这个假设的场景中,线程 1 可以解锁 mutex
,并且然后在另一个线程获得 mutex
、更改谓词、解锁 mutex
并向 condition_variable
发出信号时被中断,所有这些都在线程 1 调用 wait
之前。现在线程 1 永远休眠,因为没有线程会看到谓词需要更改,并且 condition_variable
需要发信号。
因此 势在必行 unlock
/enter-wait
自动 发生。如果 lock
/exit-wait
也自动发生,它会使 API 更容易使用。
通知的条件变量在通知后重新锁定互斥锁是什么原因
如果 unique_lock 没有作用域或者互斥量没有明确解锁,下面的代码会死锁
#include <future>
#include <mutex>
#include <iostream>
using namespace std;
int main()
{
std::mutex mtx;
std::condition_variable cv;
//simulate another working thread sending notification
auto as = std::async([&cv](){ std::this_thread::sleep_for(std::chrono::seconds(2));
cv.notify_all();});
//uncomment scoping (or unlock below) to prevent deadlock
//{
std::unique_lock<std::mutex> lk(mtx);
//Spurious Wake-Up Prevention not adressed in this short sample
//UNLESS it is part of the answer / reason to lock again
cv.wait(lk);
//}
std::cout << "CV notified\n" << std::flush;
//uncomment unlock (or scoping above) to prevent deadlock
//mtx.unlock();
mtx.lock();
//do something
mtx.unlock();
std::cout << "End may never be reached\n" << std::flush;
return 0;
}
即使重新阅读 some documentation and examples 我仍然不觉得这很明显。
可以在网上找到的大多数示例都是小型代码示例,它们具有 unique_lock.
的固有范围我们是否应该使用不同的互斥锁来处理临界区(互斥锁 1)和条件变量等待和通知(互斥锁 2)?
注意:调试显示等待阶段结束后,"internal" "mutex count"(我认为结构 __pthread_mutex_s 的字段 __count )从 1 变为 2 .解锁后回到0
您正在尝试锁定互斥体两次。一次使用 unique_lock 并再次使用显式 mutex.lock()
调用。对于非递归互斥锁,它会在重新锁定尝试时死锁,让您知道您有错误。
std::unique_lock<std::mutex> lk(mtx); // This locks for the lifetime of the unique_lock object
cv.wait(lk); // this will unlock while waiting, but relock on return
std::cout << "CV notified\n" << std::flush;
mtx.lock(); // This attempts to lock the mutex again, but will deadlock since unique_lock has already invoked mutex.lock() in its constructor.
修复非常接近那些未注释的花括号。只需确保在互斥锁上一次只有一个锁处于活动状态。
此外,如您所见,您的代码很容易被虚假唤醒。这里有一些调整给你。您应该始终停留在等待循环中,直到条件或状态(通常由互斥锁本身保护)实际发生。对于简单的通知,bool 就可以了。
int main()
{
std::mutex mtx;
std::condition_variable cv;
bool conditon = false;
//simulate another working thread sending notification
auto as = std::async([&cv, &mtx, &condition](){
std::this_thread::sleep_for(std::chrono::seconds(2));
mtx.lock();
condition = true;
mtx.unlock();
cv.notify_all();});
std::unique_lock<std::mutex> lk(mtx); // acquire the mutex lock
while (!condition)
{
cv.wait(lk);
}
std::cout << "CV notified\n" << std::flush;
//do something - while still under the lock
return 0;
}
因为条件等待可能 return 除了被通知之外的原因,例如信号,或者只是因为其他人写入了相同的 64 字节缓存行。或者它可能已收到通知但条件不再为真,因为另一个线程处理了它。
所以互斥锁被锁定,这样您的代码就可以在持有互斥锁的同时检查其条件变量。也许这只是一个布尔值,表示它已准备就绪。
不要跳过那部分。如果你这样做,你会后悔的。
让我们暂时假设 mutex
不是 从 wait
:
线程 1:
锁定 mutex
,检查谓词(无论可能是什么),并在发现谓词不是可接受的形式时,等待其他线程将其放入可接受的形式。等待 原子地 将线程 1 置于睡眠状态并解锁 mutex
。 mutex
解锁后,其他一些线程将有权将谓词置于可接受的状态(谓词自然不是线程安全的)。
线程 2:
同时,此线程正在尝试锁定 mutex
并将谓词置于线程 1 可接受的状态以继续等待。它必须在 mutex
锁定的情况下执行此操作。 mutex
保护谓词不被多个线程同时访问(读取或写入)。
一旦线程 2 将 mutex
置于可接受状态,它会通知 condition_variable
并解锁 mutex
(这两个动作的顺序与此参数无关) .
线程 1:
现在线程 1 已收到通知,我们假设 mutex
未从 wait
锁定到 return。线程 1 必须做的第一件事是检查谓词以查看它是否真的可以接受(这可能是虚假唤醒)。但是它不应该在 mutex
没有被锁定的情况下检查谓词。否则其他线程可能会在该线程检查谓词后立即更改谓词,从而使该检查的结果无效。
所以非常这个线程在醒来时必须做的第一件事是锁定mutex
,然后检查谓词。
所以mutex
从wait
锁定在return确实更方便。否则,等待线程必须在 100% 的时间内手动锁定它。
让我们再看看当线程 1 进入 wait
时的事件:我说过睡眠和解锁 自动发生 。这个非常重要。想象一下,如果线程 1 必须手动解锁 mutex
和 ,然后 调用 wait
:在这个假设的场景中,线程 1 可以解锁 mutex
,并且然后在另一个线程获得 mutex
、更改谓词、解锁 mutex
并向 condition_variable
发出信号时被中断,所有这些都在线程 1 调用 wait
之前。现在线程 1 永远休眠,因为没有线程会看到谓词需要更改,并且 condition_variable
需要发信号。
因此 势在必行 unlock
/enter-wait
自动 发生。如果 lock
/exit-wait
也自动发生,它会使 API 更容易使用。