初识C++之线程库

news2025/1/15 13:51:21

目录

一、C++中的线程使用

二、C++的线程安全问题

1. 加锁

2. 变为原子操作

3. 递归里面的锁

4. 定时锁

5. RAII的锁

三、条件变量

1. 为什么需要条件变量

2. 条件变量的使用

2.1 条件变量的相关函数

2.2 wait函数


一、C++中的线程使用

线程的概念在linux中的线程栏已经详细介绍过了,所以这里就不再赘述,以下的内容大部分都默认读者对线程概念、线程控制和线程的同步与互斥有一定了解。在这里,主要了解一下C++中的线程。

C++中的线程其实就是对各个平台的线程进行的一层封装,以便于多平台通用。上图就是在C++中的线程可能会使用到的接口。

首先来了解一下C++中的线程创建:

从上图可以看到,C++中的线程是禁止拷贝构造的,但是支持移动拷贝。原因很简单,在OS中的每个线程都有自己唯一的id,如果用拷贝构造,就可能出现两个具有相同id的线程。而移动拷贝是交换数据,就没有这个问题。

同时,C++中的线程也是用类来封装的。与linux中的线程创建出来后就必须执行任务不同,C++中可以创建空线程,即一个什么都不做的线程

利用这个特性,就可以结合C++中的一些容器,实现创建多个线程:

这里就是使用一个vector,一次性创建了3个线程。要让这些线程运行起来也比较简单,向线程传一个可调用对象即可。这个可调用对象也可以是一个lambda表达式。为了方便看到这些不同的线程,打印这些不同的线程的id。可以使用get_id()来获取线程id:

写出如下程序:

在这个程序里面的“this_thread::”是一个命名空间,它里面保存了线程相关的接口:

注意,多线程中一定要写join()来等待回收线程资源。否则主线程可能会直接运行结束导致整个程序结束。使用join后主线程就会阻塞式等待从线程结束。运行程序:

通过线程id,我们确实可以知道这里有3个不同的线程在并发运行。但是,为什么这里的打印结果是乱的呢?原因很简单,在这里是在向屏幕输出数据,在没有使用锁的情况下,会有线程安全问题。这里就是一个线程的打印还未结束,调度器就将另一个线程切进来继续打印,造成了打印不完整的问题。

如果想让一个线程被创建出来后就立即运行,就可以使用带参的构造函数:

在这里面,第一个fn是一个可调用对象。args则是一个可变参数包,是传给fn这个可调用对象的参数的:

二、C++的线程安全问题

1. 加锁

先来看以下一个程序:

在这个程序里面,有两个线程,分别执行不同的任务。但是它们的执行函数中都对全局变量val进行了++。运行该程序:

可以看到,运行结果并不是正确的。 因为++val这个动作并不是原子的。我们知道,要++一个变量,至少要经过“从内存读取数据到寄存器”,“用计算器进行计算”和“将数据写回内存”三个步骤。此时这个变量被两个不同的线程修改,此时就可能出现线程安全问题。例如当val == 10时,线程A刚++val,还没有将11写回到内存里,调度器就将线程A切换为线程B,由于val未被修改,所以线程B也拿到val == 10并进行++,然后将数据写回到内存里。此时线程A被切回来,也将11写回内存。此时就导致这两个线程将相同的值写回内存。但是线程A和线程B的循环次数已经增加了,也就导致少++了一次。多次的少++,就会导致上面的结果。

要解决这个问题,就可以进行“加锁”:

 在这里面,lock是上锁unlock是解锁。try_lock也是用于上锁的,但是和lock让线程阻塞式等待不同,try_lock在线程获取锁成功后,会正常执行。但如果获取失败,该线程就不再是在此阻塞式等待,而是可以去执行其他操作。当线程获取锁成功后,执行锁内的代码;如果失败,则返回false。写出如下程序: 

运行该程序:

可以看到,如果是lock(),那么线程就会阻塞式等待,不会进入else条件。但是这里是try_lock(),所以当线程获取锁失败后,它并不是阻塞等待,而是继续向下运行。

再来看lock()。这里就是阻塞等待。修改程序:

通过加锁的方式,让线程在加锁的范围内串行运行。运行程序:

此时程序的运行结果就是正常的。

上面是两个线程执行两个不同的函数的情况。如果将其改为执行同一个函数并运行:

依然没有问题。那有人就奇怪了,为什么++val会有线程安全问题,而这里的++i却没有呢?其实是因为每个线程都有自己的栈结构,这也就导致每个线程都会有一份自己的函数体。因此,虽然它们执行的是同一个函数,但是它们的栈中都有一份单独的函数体,里面的局部变量都是属于线程本身的。只有该进程中的共享资源,例如全局变量,才会被所有线程看到同一份。

2. 变为原子操作

要保护数据安全,还有一种方法,就是将数据的修改变为原子。要变为原子,就需要使用系统提供的“CAS操作”。CAS操作用户是无法直接使用的,只能使用系统提供的接口来实现。如果大家有兴趣,可以去了解一下CAS的具体实现。这里只简单的介绍一下。

以++val为例,假设val为1,在进行VAS操作时,会将1写到两个寄存器里面,假设这两个寄存器为eax,分别存放在两个CPU中。当val需要++时,会将val的值写入到另外两个寄存器中进行++运算,并在寄存器中保存“预期原值”1。当需要将数据写回到内存中时,会先拿预期原值与CPU中的val的值进行比对,如果相等,就写回;如果不相等,就继续计算并更新预期原值和需要写回的值,直到可以写回。通过这种方式,就实现了在同一时刻只能写回一个++的结果。

 如果想让我们自己的计算实现原子操作,就可以使用“atomic”

atomic是一个封装过的类,支持以下的原子操作:

它的底层就是使用了CAS来实现的原子操作。所以,将代码修改如下并运行:

可以看到,在原本的情况中,这里由于没有锁的保护,且val是一个全局变量,所以会出现线程安全问题,val的值不正确。但使用了atomic后,就可以得到正确的值了。当然,因为CAS的局限性,并不是所有场景都可以使用atomic。例如打印字符串。

当然,在实际中是不推荐在多线程中使用全局变量的。那如果我们想让两个线程看到同一个变量,又不使用全局变量,该怎么做呢?这里,就可以使用lambda表达式:

因为传给线程的是可调用对象,所以lambda表达式也是可以传入的。在这里,使用lambda表达式就可以让两个线程在main函数内就看见同一个变量。

3. 递归里面的锁

在递归中,最好不要用锁。因为可能造成死锁的情况。例如如下程序:

当一个线程进入该函数后,会先申请锁,然后去执行下面的代码。但是在锁保护的代码中刚好就要进行递归。调用函数,此时线程就会进入下一层的递归调用中。于是线程又需要去申请锁。但是此时所已经被改线程拿走了,于是该线程无法申请到锁,在这里等待。

为了解决这种情况,在C++的线程库中,就提供了一个“递归互斥锁”

它的接口和普通的锁是一样的:

但是它就能解决上面的递归造成的死锁问题。解决方法很简单,一个线程要申请锁,那么就必定要进入到这个锁的函数内,此时,这个函数就可以获取到该线程的id。当锁已经被申请走情况下又有线程过来申请锁,就先对比线程id,看申请锁的线程id与它保存的线程id是否相等,相等就直接进入,不相等就阻塞等待。

4. 定时锁

一般来讲,锁申请后都需要线程运行完后用unlock()释放。但有时可能会有一种特殊需求,那就是一个线程可以通过unlock()解锁,但是这个线程最多只能持有锁固定时间。一旦过了这个时间,无论是否执行完,都需要释放锁。此时就可以使用“timed_mutex”

 这个类里面既提供了正常的锁,也提供了通过时间控制的锁

至于如何具体使用,这里就不再阐述了,因为实际中的用处并不大。如果有兴趣,可以对照文档使用。

5. RAII的锁

在实际中,我们可能遇到锁保护的代码会抛异常的情况。一旦锁保护的代码中抛了异常,就会让执行流跳转到外部的catch中,进而导致线程未执行unlock(),形成死锁。

因此,遇到锁保护的代码中可能抛异常的情况,最好使用lock_guard:

这个类是对锁的封装,利用RAII的方法将锁的生命周期与作用域相绑定。一旦离开该对象的作用域,这个对象就会调用析构函数自动释放锁。

 在C++中,除了lock_guard,unique_lock也可以做到同样的事。

虽然unique_lock的作用和lock_guard的作用是一样的,都是在对象销毁时自动调用析构函数释放锁。但是unique与lock_guard相比起来,提供了更多的成员函数以供操作:

使用起来也比较灵活。

三、条件变量

1. 为什么需要条件变量

在了解条件变量前,先来看这么一个题:现在让你用两个线程交替打印奇数和偶数,打印的结果要到100。要完成这个题也是比较简单的,写出如下代码:

运行该程序:

结果没有问题。但我们再多运行几次:

可以发现,多运行几次后就会发现,打印的结果里面有时有100,有时又没有。这其实就是因为线程问题导致的。假设i == 99,当线程t1执行完++后,i == 100。此时,线程t2刚好停留在if判断的位置,在它刚准备从内存获取i的值时,线程t1将i修改为100,于是线程t2获取的i == 100。满足判断条件,进入if内执行代码。执行完后++i,i == 101,不满足循环条件,退出。但是,线程t2的循环条件是i < 100,而100刚好因为t2就处于判断条件时被修改,导致t2获取到错误的i并执行对应代码。

要解决这个问题,当然可以将线程t2的循环条件改为i <= 100,此时就必定会打印100。

但是这个程序依然存在一个问题,那就是线程在反复的执行过程中,如果条件不满足,就会在循环内一直进行判断,而调度器也会频繁的调度这个线程,导致效率降低。这就好比你点了一份外卖,你每隔几分钟就问一下商家有没有做好,不仅让商家感到烦,还会让商家因为要频繁接听你的电话和回答你的问题导致制作效率降低。

既然要让线程不频繁的判断,而是进入阻塞等待,有人就想出用锁的方式来完成:

这种写法,大家乍一看可能觉得就是线程t1先定义先运行,所以线程t1会先获取锁,然后执行代码并释放锁;然后线程t2运行,获取锁并执行代码释放锁;通过这种方式形成交替打印。但是实际并不是这样的,运行程序:

在这个程序中,绝大部分都是线程t1打印的。因为线程的互斥其实并不是遵循交替,而是“竞争”。在线程互斥中,每个线程都是竞争式的去争夺锁,而不是遵循一定的次序。这就会导致竞争力高的线程可以频繁的获取锁,而竞争力低的线程只能长期阻塞等待,偶尔才能争夺到锁。

因此,锁的方法是不可行的。面对这种情况,最好就是使用条件变量。

2. 条件变量的使用

2.1 条件变量的相关函数

条件变量可以让线程按照有序的方式进行等待。每次需要唤醒线程时,如果是唤醒单个线程,就唤醒队列中的第一个线程。当然,也可以一次性唤醒全部线程。

在这里面,wait就是让线程有序进入阻塞等待的函数。而notify_one函数就是唤醒单个线程的函数;notify_all就是唤醒全部线程的函数。而wait_for和wait_until就是在固定时间后唤醒线程的函数,一般很少用。

2.2 wait函数

条件变量因为需要被不同线程看到同一份,所以它本身也是共享资源,需要锁的保护。所以,在使用条件变量时,都需要使用锁。

但是从上图可以看到,wait函数的参数要求是unique_lock的引用,即RAII的锁。原因很简单,因为条件变量需要锁的保护,所以锁是写在wait之前的。要让多个线程在wait这里等待,就表示不能让这些线程在锁的地方等待。因此传入一个unique_lock,这个锁被传入到wait后,一旦wait函数执行完毕,就会调用它的析构函数释放锁,进而让其他线程可以获取到锁并进入wait函数等待。这里的unique_lock还有一个作用,就是在持有锁的线程被切走时,它会手动释放锁,这样就可以在持有锁的线程被切走的时候让其他线程能够获取锁

修改代码如下:

该程序中就使用了条件变量。注意,这里的条件变量的判断语句使用的是while。如果使用if,就可能出现多个线程进入同一个if判断,一旦唤醒多个线程时,就可能会导致多个线程同时离开if去执行下面的代码,进而出现错误。while循环就保证了一次最多只会有一个线程离开。

运行该程序:

打印结果正确。C++的条件变量其实还支持将判断条件放在wait函数中:

这里的pred就是判断条件,wait会根据传入的可调用对象的返回值来判断是否阻塞,true就不阻塞,false就阻塞。修改程序如下并运行:

 程序依然可以正常运行。

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

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

相关文章

9个加密货币交易所被查封,交易所安全审计后仍不安全

美国联邦调查局和乌克兰警方查封了九个加密货币交易网站&#xff0c;这些网站为包括勒索软件参与者在内的诈骗者和网络犯罪分子洗钱提供了便利。 联邦调查局 FBI 在其公告中表示&#xff0c;该行动是在虚拟货币响应小组、乌克兰国家警察和该国法律检察官的帮助下进行的。 此次…

ai皮带跑偏撕裂监测算法 yolov7

ai皮带跑偏撕裂监测系统算法基于yolov7网络模型人工智能视觉技术&#xff0c;ai皮带跑偏撕裂监测算法模型自动识别现场画面中传送皮带撕裂、跑偏、偏移等情况&#xff0c;立即告警抓拍存档同步回传后台。YOLO 的核心思想就是把目标检测转变成一个回归问题&#xff0c;利用整张图…

Git入门学习

Git是什么&#xff1f; 是一种免费开源的分布式版本控制系统&#xff0c;区别于集中式挂历系统&#xff08;SVN,CVS&#xff09;的是分布式每人都有一个“档案馆”&#xff0c;而集中式是只有一个“档案馆”。 这样的话&#xff0c;如果你使用git进行开发&#xff0c;感觉自己…

多层PCB层叠结构

在设计多层PCB电路板之前&#xff0c;设计者需要首先根据电路的规模、电路板的尺寸和电磁兼容&#xff08;EMC&#xff09;的要求来确定所采用的电路板结构&#xff0c;也就是决定采用4层&#xff0c;6层&#xff0c;还是更多层数的电路板。确定层数之后&#xff0c;再确定内电…

全景丨0基础学习VR全景制作,平台篇第18章:热点功能-音频

大家好&#xff0c;欢迎观看蛙色VR官方——后台使用系列课程&#xff01; 功能说明 应用场景 热点&#xff0c;指在全景作品中添加各种类型图标的按钮&#xff0c;引导用户通过按钮产生更多的交互&#xff0c;增加用户的多元化体验。 音频热点&#xff0c;即点击热点后会直接播…

低代码开发重要工具:jvs表单动态字段配置方式

在表单中常常会有动态字段的场景&#xff0c;也就是如下图所示&#xff0c;根据一个字段的内容选项去控制另外字段的内容展示 配置的思路&#xff1a;根据第一个单选框选择的内容&#xff0c;通过关联筛选的方式去选择第二个单选框的内容&#xff0c;那么第二个单选框的物理存储…

K8s基础7——DaemonSet控制器、Job批处理调度、Cronjob定时调度

文章目录 一、DaemonSet二、Job三、Cronjob 一、DaemonSet 控制器介绍&#xff1a; DaemonSet 可以确保全部&#xff08;或者某些&#xff09;节点上运行一个 Pod 的副本。当有节点加入集群时&#xff0c; 也会给这新节点新增一个 Pod 。当有节点从集群移除时&#xff0c;此节点…

redis最大连接数查询与设置

redis客户端连接数 redis通过监听一个TCP端口或socket的方式接收来自客户端的连接&#xff0c; 当与客户端建立连接后&#xff0c;redis内部会进行如下操作&#xff1a; (1)客户端socket会被设置为非阻塞模式&#xff0c;因为redis在网络时间处理上采用的是非阻塞多路复用模…

基于JavaWeb实现的汽车维修管理系统

【简介】 本系统基于springboot mybatis jps架构开发&#xff0c;前后端分离&#xff0c;开发环境为jdk1.8、mysql、maven。系统功能主要分为汽车维修管理、配件管理、财务管理、基础数据管理、系统维护5大模块。 【功能结构】 【技术架构】 系统架构&#xff1a;springboot …

不会做大数据实时计算?10年数据分析师整理,一文给出解决方案

本文分为四个章节介绍实时计算&#xff0c;第一节介绍实时计算出现的原因及概念&#xff1b;第二节介绍实时计算的应用场景&#xff1b;第三节介绍实时计算常见的架构&#xff1b;第四节是实时数仓解决方案。 一、实时计算 实时计算一般都是针对海量数据进行的&#xff0c;并…

RISC-V OS(老师的OS) 基于 汪辰老师的视频笔记

前言 最后面没写完&#xff0c;以后再补。。。 RISC-V OS RVOS 介绍 操作系统定义 操作系统&#xff08;英语&#xff1a;Operating System&#xff0c;缩写&#xff1a;OS&#xff09;是一组系统软件程序&#xff1a; 主管并控制计算机操作、运用和运行硬件、软件资源。提…

[oeasy]python049_帮助手册_pydoc_manual_document

帮助手册 回忆上次内容 上次了解了注释 注释是为了让程序更可读注释不会影响程序运行速度 注释分为两种 单行的 以#开头不能是字符串当中的# 多行的 三个"三个 多行注释还有什么特殊功能么&#xff1f;&#x1f914; 增加描述说明 #!/usr/bin/python3 #vim: set file…

技能梳理38@stm32+CC2530+超声波+光敏+oled

技能梳理38stm32CC2530超声波光敏oled 1、项目简介 2、实现逻辑 #主从机使用zigbee通信 #主机oled显示当前停车场位置图 #从机通过超声波和光敏一起检测是否有车 #当某位置车辆进出&#xff0c;在主机上oled进行提示 3、应用场景 #车辆无线检测 #货架商品有无检测 4、核心…

UWA Pipeline 2.4.2 版本更新说明

UWA Pipeline是一款面向游戏开发团队的本地协作平台&#xff0c;旨在为游戏开发团队搭建专属的DevOps研发交付流水线&#xff0c;提供可视化的CICD操作界面、高可用的自动化测试以及UWA性能保障服务的无缝贴合等实用功能。 在本次2.4.2版本更新中&#xff0c;我们对Pipeline的…

【Java+GS】GeoServer,通过配置自定义样式,实现不同图斑展示不同颜色。附java实现方法

文章目录 客户端发布样式一、[GeoServer中使用SLD样式](https://www.cnblogs.com/tuboshu/p/10752292.html)二、分属性渲染三、客户端操作发布图层 掌握使用java api 发布样式一、前置准备二、发布图层三、发布工具类 设计从数据库字段自定义样式一、数据库设计二、业务逻辑分享…

「容器云架构」K8s 多区域部署介绍

背景 Kubernetes的设计使得单个Kubernetes集群可以跨多个故障区域multiple failure zones运行&#xff0c;通常这些区域&#xff08;zones &#xff09;位于称为区域&#xff08;region&#xff09;的逻辑分组中。主要的云提供商将一个区域定义为一组故障区域 failure zones&am…

Linux高性能服务器编程|阅读笔记:第7章 - Linux服务器程序规范

目录 简介系列笔记7.1 日志7.1.1 Linux系统日志7.1.2 syslog函数7.2 用户信息7.2.1 UID、EUID、GID和EGID7.2.2 切换用户7.3 进程间关系7.3.1 进程组7.3.2 会话7.3.3 用ps命令查看进程关系7.4 系统资源限制7.5 改变工作目录和根目录7.6 服务器程序后台化结语简介 Hello! 非常感…

基于html+css的图展示50

准备项目 项目开发工具 Visual Studio Code 1.44.2 版本: 1.44.2 提交: ff915844119ce9485abfe8aa9076ec76b5300ddd 日期: 2020-04-16T16:36:23.138Z Electron: 7.1.11 Chrome: 78.0.3904.130 Node.js: 12.8.1 V8: 7.8.279.23-electron.0 OS: Windows_NT x64 10.0.19044 项目…

【UE】高级载具插件-02-坦克开火

1. 添加开火的操作映射 2. 创建一个actor蓝图类&#xff0c;添加一个静态网格体组件 添加发射物移动组件 设置初始速度和最大速度 发射物重力范围设为0.05 添加音频组件 设置音效 3. 打开炮管的静态网格体 在插槽管理器中创建插槽 将创建的插槽放到炮口位置 4. 打开“BP_BaseT…

《物联网安全关键技术白皮书》解读

物联网技术作为物理世界与信息世界融合的具象体现&#xff0c;有效地连接分离的物理世界和信息空间&#xff0c;囊括了传感器网络、通信网络以及互联网&#xff0c;构建物与物互联、人与物互联、人与人互联的协同共生关系&#xff0c;推进了信息产业的新变革&#xff0c;同时也…