C++ 中的线程、锁、条件变量。

news2024/10/21 9:01:50

线程 

0.Linux中有POSIX标准的线程,Boost库中也支持线程(比如thread_groupupgrade_lock ),C++11<thread>头文件中也提供了相应的API支持我们使用线程。它们三个,你学会一个,自然触类旁通。

1.创建线程:

#include<iostream>
#include<thread>
using namespace std;
void fun(){
  cout << "fun()" << endl;
}
int main(){
  std::thread t(fun);
  t.join();
  return 0;
}

编译命令:

g++ thread.cc -lpthread -std=c++11

 解释:
我们实例化一个thread对象,把fun函数地址传给它,让他去执行fun函数,在主线程中我们还让t调用了join方法,此方法会阻塞住主线程直到t执行完fun函数。也就是谁调用了join,join就会等待它执行完成。然后调用join的线程的资源会被释放。如果没有t.join();那么主线程执行完时,子线程 t 可能还没有执行完成。

 需要注意的点是:每个线程只能调用一次 join()。如果尝试对同一个线程多次调用 join(),或者对已经被 join()detach() 的线程调用 join(),会抛出 std::system_error 异常:

至于detach是什么?请看下述代码:
 

#include<iostream>
#include<thread>
using namespace std;
void fun(){
  std::this_thread::sleep_for(std::chrono::seconds(3));
  cout << "fun()done" << endl;
}
void fun2(){
  std::thread t(fun);
  t.detach();
  cout << "fun2()done" << endl;
}
int main(){
  fun2();
  std::this_thread::sleep_for(std::chrono::seconds(5));
  cout << "main thread done" << endl;
  return 0;
}

 

 调用 detach() 会使线程在后台独立运行,调用者线程不会等待它完成。有一个“脱离”的作用,而且你也不必再join它了。它会释放资源的。

如上代码:fun2作为调用者,调用了fun1函数,自己先结束了,但这不会影响fun1的执行。

2.创建线程对象时,还可以送参数进去。

3.thread没有拷贝构造函数和拷贝赋值函数,有移动构造函数和移动赋值函数。

4.在初次学习thread时,常用到两个睡眠函数。

  std::this_thread::sleep_for(std::chrono::seconds(1));//睡眠一秒模拟数据处理。
   std::this_thread::sleep_for(std::chrono::microseconds(1000)); //睡眠1000微秒也就是1秒钟。

他俩可以让当前线程睡眠若干时间,方便我们模拟一些事件。

0.多个线程访问同一资源时,为了保证数据的一致性,最简单的方式就是使用 mutex(互斥锁)。这是我们要讲的第一种锁。

std::mutex mtx;//定义一把锁

mtx.lock();//加锁

mtx.unlock();//解锁

 1.还有一种使用 lock_guard 自动加锁、解锁。它和mutex配合使用。原理是 RAII,和智能指针类似:实例化一个对象之后,这个对象的构造函数就是加锁,这个对象生存期到了调用析构时就解锁了。底层代码很简单。

std::mutex mtx;

void fun()

{

  {

  std::lock_guard<std::mutex> lock(mtx);//lock调用构造函数,对mtx加锁。

  cout << "hello world" << endl;

  }

//到这里,mtx就被自动释放了。总之我们不需要显式解锁,只需要看清楚lock的生存周期。lock在自己析构的时候会解锁。

}

2.我们还可以使用 unique_lock 自动加锁、解锁。他常常与条件变量配合使用,也可以与mutex配合。 unique_locklock_guard 原理相同,但是提供了更多功能(比如可以结合条件变量使用)。

另外:mutex::scoped_lock 其实就是 unique_lock<mutex>typedef。  至于 unique_locklock_guard 详细比较,可移步 StackOverflow。

unique_lock与条件变量配合我们在条件变量部分讲解。

条件变量

条件变量我们在Linux学习过,所以我们简单阐述:

0.什么时候用条件变量:线程 A 等待某个条件并挂起,直到线程 B 设置了这个条件,并通知条件变量,然后线程 A 被唤醒。

1.常常与unique_lock配合使用:

也就是说,你传入的参数应是一个 std::unique_lock<std::mutex> 类型的对象 。

2.条件变量被notify_all() 或者notify_one()也就是唤醒之后,之前挂起的线程就被唤醒,但是唤醒也有可能是假唤醒,因为有可能你等待的那个条件任然不满足。只是另一个线程条件满足了。所以wait一般会放在一个while循环中或者使用lambda表达式。两种是一样的:
相当于cv.wait(lock, [] { return ready; }); 相当于:while (!ready) { cv.wait(lock); }。 都是为了防止假的唤醒。

3.如(2)我们看到wait除了单参数的,还支持两个参数,是的,它有两种重载。如果是后者,第二个参数要求是一个函数对象,可以是仿函数也可以是Lambda表达式。

4.我们就这一个例子详细解释条件变量的相关API。

//注意,编译这种代码要用$ g++ a.cc -std=c++11 -lpthread。否则会收获:terminate called after throwing an instance of 'std::system_error'
//what(): Enable multithreading to use std::thread: Operation not permitted


#include<mutex>
#include<iostream>
#include<condition_variable>
#include<thread>
using namespace std;

std::mutex mtx;
std::condition_variable cv;
int flag = 0;
const int n = 10;
void fun1(char ch){
  std::unique_lock<std::mutex> lock(mtx);
  for (int i = 0; i < n; i++){
    cv.wait(lock, []() -> bool
            { return flag == 0; });
    cout << "fun1->" << ch << endl;
    flag = 1;
    cv.notify_all();
  }
}
void fun2(char ch){
  std::unique_lock<std::mutex> lock(mtx);
  for (int i = 0; i < n; i++){
    cv.wait(lock, []() -> bool
            { return flag == 1; });
    cout<< "fun2->" << ch << endl;
    flag = 2;
    cv.notify_all();
  }
}
void fun3(char ch){
  std::unique_lock<std::mutex> lock(mtx);
  for (int i = 0; i < n; i++){
    cv.wait(lock, []() -> bool
            { return flag == 2; });
    cout<< "fun3->" << ch << endl;
    flag = 0;
    cv.notify_all();
  }
}
int main(){
  std::thread t1(fun1, 'A');
  std::thread t2(fun2, 'B');
  std::thread t3(fun3, 'C');
  t1.join();
  t2.join();
  t3.join();
  return 0;
}

上述代码依次打印ABC,打印n次。

上述代码最关键的一个点是:锁加在for循环之外,进入循环之后,判断flag,也就是是不是该我打印,如果不是,把锁释放,然后把自己挂起来。如果flag就是我要的,也就是第二个参数条件为真,那么此时他不会释放锁,然后它会继续往下执行,打印,唤醒,然后执行下一次for循环,然后又遇到了wait,此时flag不是他,然后它才释放掉锁,把自己挂起来。

注意:当wait的第二个参数是lambda表达式且值为假时,那么wait的第一个参数的锁将被解掉,然后当前线程被放在等待队列中。直到被其他线程唤醒notify。

被唤醒后,它要做的第一个事是抢刚才释放的锁,一直抢,抢到后继续判断第二个表达式的真假,若第二个返回true,那wait才算调用结束,然后执行下面代码,注意此时还是持有锁的。

上述代码,我们使用了 std::unique_lock 而不是其他种类的锁。原因:
        条件变量支持它:条件变量的wait()方法需要一个 std::unique_lock 类型的锁作为参数而不是 std::lock_guard。深层次原因是 std::unique_lock 可以在等待时自动释放锁,当条件满足时再重新获取锁。而后者则要等待对象生命周期到调用析构释放锁。

        他还可以转移所有权:unique_lock 可以通过移动语义将锁的所有权转移给其他对象,而 lock_guard 不支持这个特性。

5.再看一段代码。
 

#include<iostream>
#include<condition_variable>
#include<mutex>
#include<thread>
using namespace std;
int i = 0;
std::condition_variable cv;
std::mutex mtx;
void waits(char ch)
{
  std::unique_lock<std::mutex> lock(mtx);
  while (0 == i)
  {
    cout << "thread" << ch << " is waiting." << endl;
    cv.wait(lock);//这行代码的作用:把lock中的mtx锁释放掉。把当前线程放入等待队列(就像是等待在cv这个条件变量上)。当被其他线程唤醒之后,它先做的事设法获取锁,获取到之后到达就绪状态的,等待被调度,当cv.wait(lock)这一行执行完成中。然后上去判断0==i是否满足。不满足就进来,cout之后又wait了.....
    //等待在条件变量上与获取锁时都不是就绪状态,
  }
  cout << "thread" << ch << " is done." << endl;
}
void singals(){
  std::this_thread::sleep_for(std::chrono::microseconds(1000));
  {
    std::lock_guard<std::mutex> lock(mtx);//这里我们使用了lock_guard,因为我们只想让他打印一句,然后释放掉mtx,而不是像waits中的lock一样,不断解锁加锁,只有程序结束后才释放掉mtx。不要占用。但是还是包装的mtx,这是全局唯一的。
    cout << "first signals()" << endl;
  }
  //上面的块结束,那么lock调用自己的析构函数,析构函数会释放mtx这个互斥锁。
  cv.notify_all();//这里模拟虚假唤醒,因为i的值并没有改为1
  std::this_thread::sleep_for(std::chrono::microseconds(1000));
  {
    std::lock_guard<std::mutex> lock(mtx);
    i = 1;
    cout << "second signals()" << endl;
  }
  cv.notify_all();
  cout << "singals()done" << endl;
}
int main(){
  std::thread tha(waits,'A');
  std::thread thb(waits,'B');
  std::thread thc(waits,'C');
  std::thread thd(singals);
  tha.join();
  thb.join();
  thc.join();
  thd.join();
  return 0;
}

//上述代码中的整个while(){//...}可以换成
//cout << "thread" << ch << " is waiting." << endl;
//cv.wait(lock, []() -> bool { return i == 1; });
// 通过lambda表达式的真假判断条件是否满足,判断当前线程做什么
//总之,while循环与单参数的wait配合使用。lambda表达式和两个参数的wait配合使用。

6.条件变量的wait方法如果是只有一个参数的版本。那么它在被唤醒后会做什么?

std::condition_variable cv;//定义条件变量cv

 当调用 cv.wait(lock) 时,当前线程会释放与 lock 关联的互斥锁。为什么?因为wait这个代码肯定在一个循环中,它不满足人家的条件,然后进入了函数体中,调用了wait函数。所以我们此时就把锁让出来,这样其他线程就有了获得锁的可能。避免死锁。

此时该线程被放在等待队列中,直到被notify_all() 或者notify_one()也就是唤醒之后,然后wait方法此时做的事是尝试重新获取那个互斥锁,直到获取到之后,然后因为是while循环,所以它又进行判断是否条件满足,此时如果满足,就脱离了while了,执行下面代码。不满足,说明是虚假唤醒,所以有执行了wait(),又把锁解掉,进入等待。

7.要彻底掌握条件变量,可以做一些练习:哲学家就餐问题、多个线程合作打印出1-100的值。

参考

链接

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2219922.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java-类与对象-下篇

关于类与对象&#xff0c;内容较多&#xff0c;我们分为两篇进行讲解&#xff1a; &#x1f4da; Java-类与对象-上篇&#xff1a;————<传送门:Java-类与对象-上篇-CSDN博客> &#x1f4d5; 面向对象的概念 &#x1f4d5; 类的定义格式 &#x1f4d5; 类的使用 …

特斯拉Optimus:展望智能生活新篇章

近日&#xff0c;特斯拉举办了 "WE ROBOT" 发布会&#xff0c;发布会上描绘的未来社会愿景&#xff0c;让无数人为之向往。在这场吸引全球无数媒体的直播中&#xff0c;特斯拉 Optimus 人形机器人一出场就吸引了所有观众的关注。从多家媒体现场拍摄的视频可以看出来&…

【C++】C++11新特性——右值引用,来看看怎么个事儿

&#x1f680;个人主页&#xff1a;小羊 &#x1f680;所属专栏&#xff1a;C 很荣幸您能阅读我的文章&#xff0c;诚请评论指点&#xff0c;欢迎欢迎 ~ 目录 前言一、左值引用和右值引用二、右值引用和移动语义2.1 移动构造2.2 移动赋值2.3 STL容器插入接口2.4 左值右值相互…

【C++复习】经典笔试题

文章目录 八大排序快排过程 卡特兰数反转链表链表的回文结构左叶子之和另一棵树的子树归并排序类与对象编程训练杨辉三角字符串乘积二叉树前序遍历成字符串数组的交集二叉树的非递归前序遍历连续子数组的最大乘积 八大排序 插冒归稳定 快排过程 以 [3,4,6,1,2,4,7] 为例&#…

【计网笔记】以太网

经典以太网 总线拓扑 物理层 Manchester编码 数据链路层 MAC子层 MAC帧 DIX格式与IEEE802.3格式 IEEE802.3格式兼容DIX格式 前导码&#xff08;帧开始定界符SOF&#xff09; 8字节 前7字节均为0xAA第8字节为0xAB前7字节的Manchester编码将产生稳定方波&#xff0c;用于…

steam游戏模拟人生3缺少net framework 3.5安装不成功错误弹窗0x80070422怎么修复

模拟人生3在Steam上运行时提示缺少.NET Framework 3.5并出现错误代码0x80070422&#xff0c;通常意味着.NET Framework 3.5功能没有正确启用&#xff0c;或者安装过程中出现了问题。以下是解决这个问题的步骤&#xff1a; 1.启用Windows功能 按下Win R键&#xff0c;输入opti…

【论文学习与撰写】论文里的Mathtype公式复制粘贴,跨文档复制后错码/错位问题的解决

1、描述 问题&#xff1a;论文的草稿已经写好&#xff0c;里面的公式之类的都已经一个个打上去了 但是把草稿里的正文和公式粘贴在另一个文档里的时候&#xff0c;会出些公式格式错误的情况 那该怎么操作保证复制后的公式保持原格式呢 选中复制的内容&#xff0c;在另一个文…

MySQL【知识改变命运】10

联合查询 0.前言1.联合查询在MySQL里面的原理2.练习一个完整的联合查询2.1.构造练习案例数据2.2 案例&#xff1a;⼀个完整的联合查询的过程2.2.1. 确定参与查询的表&#xff0c;学⽣表和班级表2.2.2. 确定连接条件&#xff0c;student表中的class_id与class表中id列的值相等2.…

【c++篇】:解析c++类--优化编程的关键所在(一)

文章目录 前言一.面向过程和面向对象二.c中的类1.类的引入2.类的定义3.类的封装和访问限定符4.类的作用域5.类的实例化6.类对象模型 三.this指针1.this指针的引出2.this指针的特性3.C语言和c实现栈Stack的对比 前言 在程序设计的广袤宇宙中&#xff0c;C以其强大的功能和灵活性…

[k8s理论知识]6.k8s调度器

k8s默认调度器 k8s调度器的主要职责&#xff0c;就是为一个新创建出来的pod寻找一个适合的节点Node。这包括两个步骤&#xff0c;第一&#xff0c;从所有集群的节点中&#xff0c;根据调度算法挑选出所有可以运行该pod的节点&#xff0c;第二&#xff0c;从第一步的结果中&…

Java项目-基于springboot框架的企业客户信息反馈系统项目实战(附源码+文档)

作者&#xff1a;计算机学长阿伟 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、ElementUI等&#xff0c;“文末源码”。 开发运行环境 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringBoot、Vue、Mybaits Plus、ELementUI工具&#xff1a;IDEA/…

Windows环境下Qt Creator调试模式下qDebug输出中文乱码问题

尝试修改系统的区域设置的方法&#xff1a; 可以修复问题。但会出现其它问题&#xff1a; 比如某些软件打不开&#xff0c;或者一些软件界面的中文显示乱码&#xff01; 暂时没有找到其它更好的办法。

10-Docker安装Redis

10-Docker安装Redis Docker安装Redis 以 Redis 6.0.8 为例&#xff1a; docker pull redis:6.0.8直接pull会出现以下错误 [rootdocker ~]# docker pull redis:6.0.8 Error response from daemon: Get "https://registry-1.docker.io/v2/": net/http: request can…

[Python学习日记-50] Python 中的序列化模块 —— pickle 和 json

[Python学习日记-50] Python 中的序列化模块 —— pickle 和 json 简介 pickle 模块 json 模块 pickle VS json 简介 什么叫序列化&#xff1f; 序列化指的是将对象转换为可以在网络上传输或者存储到文件系统中的字节流的过程。序列化使得对象可以被保存、传输和恢复&#…

3D Slicer 教程二 ---- 数据集

上一章下载3d slicer的软件,这章从加载数据集来弄清楚3dslicer怎么使用. 一. 加载数据集 如果没有数据集,也可用用样本数据. (1) "File" --> "add Data" 可以添加图片文件夹,(试了MP4不行,内镜的视频估计不支持),添加单个图片的话,会出现一些选项, …

C++贪心

前言 C算法与数据结构 打开打包代码的方法兼述单元测试 简介 贪心算法&#xff08;Greedy Algorithm&#xff09;是一种在每一步选择中都采取在当前状态下最好或最优&#xff08;即最有利&#xff09;的选择&#xff0c;从而希望导致结果是全局最好或最优的算法策略。贪心算…

【设计模式系列】抽象工厂模式

一、什么是抽象工厂模式 抽象工厂模式&#xff08;Abstract Factory Pattern&#xff09;是一种创建型设计模式&#xff0c;它提供了一个接口&#xff0c;用于创建一系列相关或相互依赖的对象&#xff0c;而无需指定它们具体的类。这种模式允许客户端使用抽象的接口来创建一组…

AUTOSAR_EXP_ARAComAPI的5章笔记(17)

☞返回总目录 相关总结&#xff1a;AutoSar AP CM通信组总结 5.7 通信组 5.7.1 目标 通信组&#xff08;Communication Group&#xff0c;CG&#xff09;是由 AUTOSAR 定义的复合服务模板。它提供了一个通信框架&#xff0c;允许在 AUTOSAR 应用程序之间以对等方式和广播模…

第6章 元素应用CSS作业

1.使用CSS对页面网页元素加以修饰&#xff0c;制作“旅游攻略”网站。 浏览效果如下&#xff1a; HTML代码如下&#xff1a; <!DOCTYPE html> <html><head><meta charset"utf-8"><title>旅游攻略</title><link type"t…

[JAVAEE] 线程安全问题

目录 一. 什么是线程安全 二. 线程安全问题产生的原因 三. 线程安全问题的解决 3.1 解决修改操作不是原子性的问题 > 加锁 a. 什么是锁 b. 没有加锁时 c. 加锁时 d. 死锁 e. 避免死锁 3.2 解决内存可见性的问题 > volatile关键字 (易变的, 善变的) a. 不加…