C++ | 多线程编程(三) 条件变量
2023-09-27 14:28:31 时间
条件变量:https://zh.cppreference.com/w/cpp/thread/condition_variable
condition_variable 类是同步原语,能用于阻塞一个线程,或同时阻塞多个线程,直至另一线程修改共享变量(条件)并通知 condition_variable 。
条件变量一般与互斥锁一起使用。
三个线程轮流打印0~100.
目标:是三个线程分别依次打印1,2,3…100
#include <iostream>
#include <thread>
#include <mutex> // 互斥量
#include <condition_variable> // 条件变量
using namespace std;
/*
// 版本一:不加信号量,互斥量,也可以完成,但会造成“空耗cpu”
例如在fun0中,不满足number%3==0条件,
则一直进行循环,直至线程时间片到达,切换线程
*/
void fun0() // 0 3
{
while (number <= 100)
{
while (number % 3 == 0 && number <= 100)
{
cout << "t0_" << number << " ";
number += 1;
}
//cout << ".";
}
}
void fun1() // 1 4
{
while (number <= 100)
{
while (number % 3 == 1 && number <= 100)
{
cout << "t1_" << number << " ";
number += 1;
}
//cout << ".";
}
}
void fun2() // 2 5
{
while (number <= 100)
{
while (number % 3 == 2 && number <= 100)
{
cout << "t2_" << number << " ";
number += 1;
}
//cout << ".";
}
}
int main()
{
thread t0(fun0);
thread t1(fun1);
thread t2(fun2);
t0.join();
t1.join();
t2.join();
return 0;
}
缺点:会使得CPU空耗大量时间。取消上边的cout << ".";
注释可以看到cpu空耗情况。
使用条件变量
条件变量:
notify_one()(随机唤醒一个等待的线程)
notify_all()(唤醒所有等待的线程)
wait()是条件变量的成员函数,堵塞,等待唤醒
如果第二个参数lambda表达式返回值是false,
那么wait将解锁第一个参数(互斥量),并堵塞到本行。
堵塞到其他某个线程调用notify_one()成员函数为止
notify_one();//尝试吧wait的线程唤醒,执行完这行,wait就被唤醒了
当其他notify_one()将wait唤醒之后,wait不断尝试获取互斥量锁,
如果获取不到,流程就卡在wait这里等着获取。
如果获取到了,wait就获取到锁(就等于上锁);
这里存在一个问题,在下面的程序片段中,我们有Aa方案和Bb方案使用条件变量。
void fun0() // 0 3
{
unique_lock < std::mutex> locker(g_mtx); // 唯一性锁
while (number <= 100)
{
while (number % 3 == 0 && number <= 100)
{
cout << "t0_" << number << " ";
number += 1;
// cv.wait(locker); // 等待 A ,A-a 先等待后唤醒
// 阻塞,等待其他线程唤醒
cv.notify_one(); // 唤醒 B ,B-b 先唤醒后等待
// 通知别的线程,唤醒其他线程
}
cout << ".a";
// cv.notify_one(); // 唤醒 a
cv.wait(locker); // 等待 b
}
cv.notify_one(); // 唤醒最后一个阻塞线程
}
等待和唤醒调用的时机很重要,如果我们使用Aa方案,那么可能会出现两种情况。
- 第一个执行到此处的线程将会进入等待状态,而其他线程因为没要满足进入循环的条件将会空耗时间。
- 三个线程刚好顺序执行,则三个线程最后都陷入了等待状态,但是由于没有额外的线程进行唤醒,会使得程序陷入循环等待状态。
因此,我们需要使用Bb方案。
/*
// 版本二:加锁
*/
void fun0() // 0 3
{
unique_lock < std::mutex> locker(g_mtx); // 唯一性锁
while (number <= 100)
{
while (number % 3 == 0 && number <= 100)
{
cout << "t0_" << number << " ";
number += 1;
cv.notify_one(); // 唤醒 B ,B-b 先唤醒后等待
// 通知别的线程,唤醒其他线程
}
cout << ".";
cv.wait(locker); // 等待 b
}
cv.notify_one(); // 唤醒最后一个阻塞线程
}
void fun1() // 1 4
{
unique_lock < std::mutex> locker(g_mtx);
while (number <= 100)
{
while (number % 3 == 1 && number <= 100)
{
cout << "t1_" << number << " ";
number += 1;
cv.notify_one();
}
cout << ".";
cv.wait(locker);
}
// 打印100,最后阻塞线程为fun1
cv.notify_one();
}
void fun2() // 2 5
{
unique_lock < std::mutex> locker(g_mtx);
while (number <= 100)
{
while (number % 3 == 2 && number <= 100)
{
cout << "t2_" << number << " ";
number += 1;
cv.notify_one();
}
cout << ".";
cv.wait(locker);
}
cv.notify_one();
}
int main()
{
thread t0(fun0);
thread t1(fun1);
thread t2(fun2);
t0.join();
t1.join();
t2.join();
return 0;
}
此方案依然存在问题。
- 问题1:唤醒的线程,如果不是我们需要的线程,就会空耗被唤醒线程直至再次唤醒其他线程,这个过程一直到我们需要的线程被唤醒。(有可能出现死锁情况)
// 在程序中,输出两个‘.’表示有空耗 - 解决方案:使用 notify_all 唤醒所有其他线程
- 问题2:最后一个线程使用wait后,阻塞,不会有其他线程将其唤醒
- 解决方案:在最后一个线程wait时,在其他的任意一个可以正常退出的线程中调用一次唤醒程序,比如打印100个数,fun1最后退出,可以在fun0,或fun2中调用一次唤醒函数
如下图所示出现死锁情况:
修改方案,使用notify_all唤醒
/*
// 版本三:加锁
使用notify_all
线程退出时,调用一次唤醒函数
*/
void fun0() // 0 3
{
unique_lock < std::mutex> locker(g_mtx); // 唯一性锁
while (number <= 100)
{
while (number % 3 == 0 && number <= 100)
{
cout << "t0_" << number << " ";
number += 1;
// cv.wait(locker); // 等待 A ,A-a 先等待后唤醒
// 阻塞,等待其他线程唤醒
cv.notify_all(); // 唤醒 B ,B-b 先唤醒后等待
// 通知别的线程,唤醒其他线程
}
cout << ".";
// cv.notify_one(); // 唤醒 a
cv.wait(locker); // 等待 b
}
cv.notify_all(); // 唤醒最后一个阻塞线程
}
void fun1() // 1 4
{
unique_lock < std::mutex> locker(g_mtx);
while (number <= 100)
{
while (number % 3 == 1 && number <= 100)
{
cout << "t1_" << number << " ";
number += 1;
cv.notify_all();
}
cout << ".";
cv.wait(locker);
}
// 打印100,最后阻塞线程为fun1
cv.notify_all();
}
void fun2() // 2 5
{
unique_lock < std::mutex> locker(g_mtx);
while (number <= 100)
{
while (number % 3 == 2 && number <= 100)
{
cout << "t2_" << number << " ";
number += 1;
cv.notify_all();
}
cout << ".";
cv.wait(locker);
}
cv.notify_all();
}
int main()
{
thread t0(fun0);
thread t1(fun1);
thread t2(fun2);
t0.join();
t1.join();
t2.join();
return 0;
}
相关文章
- C++多线程同步技巧(四)--- 信号量
- C++多线程同步技巧(三)--- 互斥体
- C++多线程同步技巧(二) ---事件
- linux c++ 服务器端开发面试必看书籍
- 多线程 用户级线程和内核级线程 from C++多核高级编程
- 《C++多线程编程实战》——1.2 创建C++项目
- 《C++多线程编程实战》——1.3 程序结构、执行流和运行时对象
- 《C++多线程编程实战》——1.4 结构化编程方法
- 《C++多线程编程实战》——1.7 理解多态
- 《C++多线程编程实战》——1.8 事件处理器和消息传递接口
- 《C++多线程编程实战》——1.9 链表、队列和栈示例
- 《C++多线程编程实战》——2.6 解决典型的IPC问题
- 《C++多线程编程实战》导读
- 《C++覆辙录》——2.10:静态连接型别和外部连接型别
- 《C++面向对象高效编程(第2版)》——3.6 const 成员函数的概念
- C++ 11的移动语义
- 第十二届蓝桥杯大赛软件赛省赛 C/C++ 大学 B 组(第一场真题 + 部分题解)
- C++ 多线程阻塞 (多线程同步)(MsgWaitForMultipleObjects)(连着消息一起控制,牛)
- C++11 std::unique_lock与std::lock_guard区别及多线程应用实例
- OpenMP与C++:事半功倍地获得多线程的好处
- 引用内部函数绑定机制,R转义字符,C++引用,别名,模板元,宏,断言,C++多线程,C++智能指针
- C/C++教程 第十九章 —— 数据库的理解与使用
- C/C++ Windows API——多线程加锁与临界区域