既看既用
std::mutex mu;
std::condition_variable cond;
void function_1() {
……
{
std::unique_lock<std::mutex> locker(mu);
q.push_front(count);
locker.unlock();
cond.notify_one(); //唤醒线程
}// 自动解锁
……
}
}
void function_2() {
……
while ( data != 1) {
std::unique_lock<std::mutex> locker(mu); //自动锁定
cond.wait(locker,[]{return !q.empty();}); //获取锁,然后判断[]{return !q.empty()}是否为true,如果是则进入wait
……
}
}
或
void function_2() {
……
while ( data != 1) {
std::unique_lock<std::mutex> locker(mu);
while(q.empty())
cond.wait(locker); // Unlock mu and wait to be notified
……
}
}
使用while (q.empty()) cond.wait(locker)
循环是为了处理虚假唤醒(spurious wakeup),cond.wait(lock, predicate)
实际上是对 while
循环的封装。
虚假唤醒(Spurious Wakeup)
虚假唤醒是指线程在等待条件变量时,可能会在没有收到明确通知(notify_one
或 notify_all
)的情况下被唤醒。这种行为是由操作系统或底层线程库的实现引起的,是条件变量的一个固有特性。
详细
条件变量可减少判断次数(减少CPU消耗)
互斥锁std::mutex
是一种最常见的线程间同步的手段,但是在有些情况下不太高效。
假设想实现一个简单的消费者生产者模型,一个线程往队列中放入数据,一个线程往队列中取数据,取数据前需要判断一下队列中确实有数据,由于这个队列是线程间共享的,所以,需要使用互斥锁进行保护,一个线程在往队列添加数据的时候,另一个线程不能取,反之亦然。用互斥锁实现如下:
#include <iostream>
#include <deque>
#include <thread>
#include <mutex>
std::deque<int> q;
std::mutex mu;
void function_1() {
int count = 10;
while (count > 0) {
std::unique_lock<std::mutex> locker(mu);
q.push_front(count);
locker.unlock();
std::this_thread::sleep_for(std::chrono::seconds(1));
count--;
}
}
void function_2() {
int data = 0;
while ( data != 1) {
std::unique_lock<std::mutex> locker(mu);
if (!q.empty()) {
data = q.back();
q.pop_back();
locker.unlock();
std::cout << "t2 got a value from t1: " << data << std::endl;
} else {
locker.unlock();
}
}
}
int main() {
std::thread t1(function_1);
std::thread t2(function_2);
t1.join();
t2.join();
return 0;
}
//输出结果
//t2 got a value from t1: 10
//t2 got a value from t1: 9
//t2 got a value from t1: 8
//t2 got a value from t1: 7
//t2 got a value from t1: 6
//t2 got a value from t1: 5
//t2 got a value from t1: 4
//t2 got a value from t1: 3
//t2 got a value from t1: 2
//t2 got a value from t1: 1
可以看到,互斥锁其实可以完成这个任务,但是却存在着性能问题。
首先,function_1
函数是生产者,在生产过程中,std::this_thread::sleep_for(std::chrono::seconds(1));
表示延时1s
,所以这个生产的过程是很慢的;function_2
函数是消费者,存在着一个while
循环,只有在接收到表示结束的数据的时候,才会停止,每次循环内部,都是先加锁,判断队列不空,然后就取出一个数,最后解锁。所以说,在1s
内,做了很多无用功!这样的话,CPU占用率会很高,可能达到100%(单核)。如图:
CPU占用率.png
解决办法之一是给消费者也加一个小延时,如果一次判断后,发现队列是空的,就惩罚一下自己,延时500ms
,这样可以减小CPU的占用率。
void function_2() {
int data = 0;
while ( data != 1) {
std::unique_lock<std::mutex> locker(mu);
if (!q.empty()) {
data = q.back();
q.pop_back();
locker.unlock();
std::cout << "t2 got a value from t1: " << data << std::endl;
} else {
locker.unlock();
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
}
}
如图:
使用延时的CPU占用率.png
然后困难之处在于,如何确定这个延时时间呢,假如生产者生产的很快,消费者却延时500ms
,也不是很好,如果生产者生产的更慢,那么消费者延时500ms
,还是不必要的占用了CPU。
这就引出了条件变量(condition variable),c++11
中提供了#include <condition_variable>
头文件,其中的std::condition_variable
可以和std::mutex
结合一起使用,其中有两个重要的接口,notify_one()
和wait()
,wait()
可以让线程陷入休眠状态,在消费者生产者模型中,如果生产者发现队列中没有东西,就可以让自己休眠,但是不能一直不干活啊,notify_one()
就是唤醒处于wait
中的其中一个条件变量(可能当时有很多条件变量都处于wait
状态)。那什么时刻使用notify_one()
比较好呢,当然是在生产者往队列中放数据的时候了,队列中有数据,就可以赶紧叫醒等待中的线程起来干活了。
使用条件变量修改后如下:
#include <iostream>
#include <deque>
#include <thread>
#include <mutex>
#include <condition_variable>
std::deque<int> q;
std::mutex mu;
std::condition_variable cond;
void function_1() {
int count = 10;
while (count > 0) {
std::unique_lock<std::mutex> locker(mu);
q.push_front(count);
locker.unlock();
cond.notify_one(); // Notify one waiting thread, if there is one.
std::this_thread::sleep_for(std::chrono::seconds(1));
count--;
}
}
void function_2() {
int data = 0;
while ( data != 1) {
std::unique_lock<std::mutex> locker(mu);
while(q.empty())
cond.wait(locker); // Unlock mu and wait to be notified
data = q.back();
q.pop_back();
locker.unlock();
std::cout << "t2 got a value from t1: " << data << std::endl;
}
}
int main() {
std::thread t1(function_1);
std::thread t2(function_2);
t1.join();
t2.join();
return 0;
}
此时CPU的占用率也很低。
使用条件变量时的CPU占用率.png
上面的代码有三个注意事项:
- 在
function_2
中,在判断队列是否为空的时候,使用的是while(q.empty())
,而不是if(q.empty())
,这是因为wait()
从阻塞到返回,不一定就是由于notify_one()
函数造成的,还有可能由于系统的不确定原因唤醒(可能和条件变量的实现机制有关),这个的时机和频率都是不确定的,被称作伪唤醒,如果在错误的时候被唤醒了,执行后面的语句就会错误,所以需要再次判断队列是否为空,如果还是为空,就继续wait()
阻塞。 - 在管理互斥锁的时候,使用的是
std::unique_lock
而不是std::lock_guard
,而且事实上也不能使用std::lock_guard
,这需要先解释下wait()
函数所做的事情。可以看到,在wait()
函数之前,使用互斥锁保护了,如果wait
的时候什么都没做,岂不是一直持有互斥锁?那生产者也会一直卡住,不能够将数据放入队列中了。所以,wait()
函数会先调用互斥锁的unlock()
函数,然后再将自己睡眠,在被唤醒后,又会继续持有锁,保护后面的队列操作。而lock_guard
没有lock
和unlock
接口,而unique_lock
提供了。这就是必须使用unique_lock
的原因。 - 使用细粒度锁,尽量减小锁的范围,在
notify_one()
的时候,不需要处于互斥锁的保护范围内,所以在唤醒条件变量之前可以将锁unlock()
。
还可以将cond.wait(locker);
换一种写法,wait()
的第二个参数可以传入一个函数表示检查条件,这里使用lambda
函数最为简单,如果这个函数返回的是true
,wait()
函数不会阻塞会直接返回,如果这个函数返回的是false
,wait()
函数就会阻塞着等待唤醒,如果被伪唤醒,会继续判断函数返回值。
void function_2() {
int data = 0;
while ( data != 1) {
std::unique_lock<std::mutex> locker(mu);
cond.wait(locker, [](){ return !q.empty();} ); // Unlock mu and wait to be notified
data = q.back();
q.pop_back();
locker.unlock();
std::cout << "t2 got a value from t1: " << data << std::endl;
}
}
除了notify_one()
函数,c++
还提供了notify_all()
函数,可以同时唤醒所有处于wait
状态的条件变量。
两种用法
while (q.empty()) { cond.wait(locker); // 解锁并等待通知 }
工作原理
-
检查条件:
-
在进入
wait
之前,先检查条件q.empty()
。 -
如果条件为真(队列为空),则进入等待状态。
-
-
解锁并等待:
-
调用
cond.wait(locker)
时,会自动释放锁(locker
),并让当前线程进入等待状态。
-
-
被唤醒后:
-
当其他线程调用
cond.notify_one()
或cond.notify_all()
时,当前线程被唤醒。 -
被唤醒后,
cond.wait(locker)
会重新获取锁(locker
)。 -
然后再次检查条件
q.empty()
,如果条件仍然为真,则继续等待。
-
特点
-
手动检查条件:
-
需要显式地使用
while
循环来检查条件。 -
这种模式称为 "条件等待循环",是条件变量的经典用法。
-
-
潜在问题:
-
如果条件变量的通知是虚假唤醒(spurious wakeup),线程可能会被错误地唤醒,因此需要循环检查条件。
-
2. cv.wait(lock, [] { return !q.empty(); });
代码示例
cpp
复制
cv.wait(lock, [] { return !q.empty(); }); // 等待队列非空
工作原理
-
检查条件:
-
cv.wait
的第二个参数是一个谓词(lambda 函数[] { return !q.empty(); }
)。 -
在进入等待状态之前,
cv.wait
会先调用谓词检查条件。
-
-
解锁并等待:
-
如果谓词返回
false
(队列为空),则调用cv.wait
会自动释放锁(lock
),并让当前线程进入等待状态。
-
-
被唤醒后:
-
当其他线程调用
cv.notify_one()
或cv.notify_all()
时,当前线程被唤醒。 -
被唤醒后,
cv.wait
会重新获取锁(lock
),并再次调用谓词检查条件。 -
如果谓词返回
true
(队列非空),则cv.wait
返回,继续执行后续代码。 -
如果谓词返回
false
(队列仍为空),则继续等待。
-
特点
-
自动检查条件:
-
cv.wait
内部会自动处理条件检查和循环等待,避免了手动编写while
循环。
-
-
简洁性:
-
代码更简洁,逻辑更清晰。
-
避免了手动处理虚假唤醒的问题。
-
3. 区别对比
特性 | while(q.empty()) cond.wait(locker); | cv.wait(lock, [] { return !q.empty(); }); |
---|---|---|
条件检查 | 需要手动编写 while 循环检查条件。 | 自动通过谓词检查条件。 |
虚假唤醒处理 | 需要手动处理虚假唤醒(通过 while 循环)。 | 自动处理虚假唤醒。 |
代码简洁性 | 代码稍显冗长。 | 代码更简洁。 |
可读性 | 需要理解条件变量的经典用法。 | 更直观,逻辑更清晰。 |
适用场景 | 适用于需要复杂条件检查的场景。 | 适用于简单的条件检查场景。 |
4. 推荐用法
-
推荐使用
cv.wait(lock, predicate)
:-
这种方式更简洁、更安全,避免了手动处理虚假唤醒的问题。
-
适用于大多数简单的条件等待场景。
-
-
使用
while(condition) cond.wait(locker)
:-
如果条件检查逻辑非常复杂,或者需要额外的处理逻辑,可以使用这种方式。
-
适用于需要更灵活控制的场景。
-
参考
作者:StormZhu
链接:https://www.jianshu.com/p/c1dfa1d40f53
来源:简书
join的必要:
举个例子,现在有 A, B, C 三件事情,只有做完 A 和 B 才能去做 C,而 A 和 B 可以并行完成。
int main(){
thread t = new thread(A);
B(); // 此时 A 与 B 并行进行
t.join(); // 确保 A 完成
C();
}