前面学习了互斥量似乎我们就可以多线程编程了,多线程也不过如此嘛。然而我们上手coding,用多线程来结局我们实际需求就会发现,似乎多线程也不是很好用。因为我们实际对于多线程的需求,往往线程都是while循环让其不断轮询执行,而不是简单的顺序命令执行完就结束线程了。让我们看个具体的例子深入感受下其中的痛点。
//cond_var1.cpp用互斥锁实现一个生产者消费者模型
#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)); //延时1秒
count--;
}
}
//消费者,从队列提取数据
void function_2() {
int data = 0;
int f2Count = 0;
while ( data != 1) {
f2Count++;
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;
std::cout << "t2 run times " << f2Count << std::endl;
} else {
locker.unlock();
}
}
}
int main() {
std::thread t1(function_1);
std::thread t2(function_2);
t1.join();
t2.join();
getchar();
return 0;
}
功能很简单,模拟最常见的收发多线程需求,压数据还好,1s往里面写一个数据,但是接收线程啥时候知道你啥时候有数据了呢?function_2线程就只能不断轮询了,看下执行结果,短短10s,轮询了3亿多次,cpu负荷拉满,我们生产要是写这种代码,老板分分钟鱿鱼套餐走起。
一个简单的功能就把我们cpu占满了,那如何解决呢,聪明的同学已经知道了,定时轮询,cpu的轮询频率是很夸张的,现在好一点的都有几GHZ,那我们不依赖cpu轮询频率,增加线程休眠时间,别这么快,遭不住了,其实我看到的很多小公司对性能要求不高的程序确实都这么干的,虽简单,但有效。
上代码
//cond_var1.cpp用互斥锁实现一个生产者消费者模型
#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)); //延时1秒
count--;
}
}
//消费者,从队列提取数据
void function_2() {
int data = 0;
int f2Count = 0;
while ( data != 1) {
f2Count++;
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;
std::cout << "t2 run times " << f2Count << std::endl;
} else {
locker.unlock();
}
std::this_thread::sleep_for(std::chrono::milliseconds(50)); //延时50毫秒
}
}
int main() {
std::thread t1(function_1);
std::thread t2(function_2);
t1.join();
t2.join();
getchar();
return 0;
}
执行结果
频率降到180了,对于绝大多数生产环境已经算是能接受了,但是也显而易见的,他的反应时间已经不是前面的几千万分之一秒了,而升高到了50ms,50ms似乎不长,但是对于一些对时间高敏感的fps游戏等需求呢?降低休眠时间?可行,但是cpu负荷增加,这时候我们就需要一个东西来满足休眠一个线程,直到符合某个条件时再继续往下执行,这样就不用再左右为难了。
C++标准库在< condition_variable >中提供了条件变量,借由它,一个线程可以唤醒一个或多个其他等待中的线程。原则上,条件变量的运作如下:
- 你必须同时包含< mutex >和< condition_variable >,并声明一个mutex和一个condition_variable变量;
- 那个通知“条件已满足”的线程(或多个线程之一)必须调用notify_one()或notify_all(),以便条件满足时唤醒处于等待中的一个条件变量;
- 那个等待"条件被满足"的线程必须调用wait(),可以让线程在条件未被满足时陷入休眠状态,当接收到通知时被唤醒去处理相应的任务;
上代码
//cond_var2.cpp用条件变量解决轮询间隔难题
#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(); // 向一个等待线程发出“条件已满足”的通知
std::this_thread::sleep_for(std::chrono::seconds(1)); //延时1秒
count--;
}
}
//消费者,从队列提取数据
void function_2() {
int data = 0;
while ( data != 1) {
std::unique_lock<std::mutex> locker(mu);
while(q.empty()) //判断队列是否为空
cond.wait(locker); // 解锁互斥量并陷入休眠以等待通知被唤醒,被唤醒后加锁以保护共享数据
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();
getchar();
return 0;
}
执行结果
发送线程通知接收线程的时间损耗是多久呢,我们打印下收发时间
//cond_var2.cpp用条件变量解决轮询间隔难题
#include <iostream>
#include <deque>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <chrono>
#include <time.h>
std::deque<int> q; //双端队列标准容器全局变量
std::mutex mu; //互斥锁全局变量
std::condition_variable cond; //全局条件变量
//毫秒级时间戳
std::time_t getTimeStamp()
{
std::chrono::time_point<std::chrono::system_clock,std::chrono::milliseconds> tp = std::chrono::time_point_cast<std::chrono::milliseconds>(std::chrono::system_clock::now());//获取当前时间点
std::time_t timestamp = tp.time_since_epoch().count(); //计算距离1970-1-1,00:00的时间长度
return timestamp;
}
//生产者,往队列放入数据
void function_1() {
int count = 10;
while (count > 0) {
std::unique_lock<std::mutex> locker(mu);
q.push_front(count); //数据入队锁保护
time_t timestamp = getTimeStamp();
locker.unlock();
cond.notify_one(); // 向一个等待线程发出“条件已满足”的通知
std::cout << "ptime:" << timestamp << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1)); //延时1秒
count--;
}
}
//消费者,从队列提取数据
void function_2() {
int data = 0;
while ( data != 1) {
std::unique_lock<std::mutex> locker(mu);
while(q.empty()) //判断队列是否为空
cond.wait(locker); // 解锁互斥量并陷入休眠以等待通知被唤醒,被唤醒后加锁以保护共享数据
data = q.back();
time_t timestamp = getTimeStamp();
q.pop_back(); //数据出队锁保护
locker.unlock();
std::cout << "rtime:" << timestamp << std::endl;
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();
getchar();
return 0;
}
看运行结果,我们发现算上unlock(),通知以及切换线程、获取时间戳的总损耗控制再1ms以内,这完全可接受了。
这样看来条件向量算是很优秀的控制切换方案了,但是使用上我是有个疑问的,function_2在调用cond.wait()之前是把mu锁住的,现在线程又阻塞在这里休眠没有unlock(),那function_1不是会死锁吗?为了模拟这种情况,我写了个例子,线程function_2,lock()后休眠1000s,实际function_1确实阻塞住了。
//cond_var1.cpp用互斥锁实现一个生产者消费者模型
#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::cout << "t1 push a value to q: " << count << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1)); //延时1秒
count--;
}
}
//消费者,从队列提取数据
void function_2() {
int data = 0;
int f2Count = 0;
while ( data != 1) {
f2Count++;
std::unique_lock<std::mutex> locker(mu);
std::this_thread::sleep_for(std::chrono::seconds(1000)); //休眠1000s
if (!q.empty()) { //判断队列是否为空
data = q.back();
q.pop_back(); //数据出队锁保护
locker.unlock();
std::cout << "t2 got a value from t1: " << data << std::endl;
std::cout << "t2 run times " << f2Count << std::endl;
} else {
locker.unlock();
}
std::this_thread::sleep_for(std::chrono::milliseconds(50)); //延时50毫秒
}
}
int main() {
std::thread t1(function_1);
std::thread t2(function_2);
t1.join();
t2.join();
getchar();
return 0;
}
运行结果
而前面的例子,我们可以看到cond.wait导致function_2阻塞后,并不影响function_1的执行,这里就可以知道,wait后的mutex锁,是可以被再次lock()往下执行的,而且wait被唤起往下执行的时候,是需要unlock掉wait前的锁定。实际上wait被调用时该mutex必须被unique_lock锁定,否则会发生不明确的行为。