当前位置: 首页 > news >正文

初识C++之线程库

目录

一、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就阻塞。修改程序如下并运行:

 程序依然可以正常运行。

相关文章:

初识C++之线程库

目录 一、C中的线程使用 二、C的线程安全问题 1. 加锁 2. 变为原子操作 3. 递归里面的锁 4. 定时锁 5. RAII的锁 三、条件变量 1. 为什么需要条件变量 2. 条件变量的使用 2.1 条件变量的相关函数 2.2 wait函数 一、C中的线程使用 线程的概念在linux中的线程栏已经…...

ChatGLM-LLaMA-chinese-insturct 学习记录(含LoRA的源码理解)

ChatGLM-LLaMA-chinese-insturct 前言一、实验记录1.1 环境配置1.2 代码理解1.2.1 LoRA 1.4 实验结果 二、总结 前言 介绍&#xff1a;探索中文instruct数据在ChatGLM, LLaMA等LLM上微调表现&#xff0c;结合PEFT等方法降低资源需求。 Github: https://github.com/27182812/Ch…...

JuiceFS-K8s部署

目录 1、部署JuiceFS-CSI驱动2、创建OBS认证信息Secret3、创建存储类4、创建PVC--PVC创建时会自动创建PV5、创建测试Pod--测试Pod创建容器内是否挂载成功 官网文档地址&#xff1a;https://juicefs.com/docs/zh/csi/introduction/ 1、部署JuiceFS-CSI驱动 部署yaml如下&#x…...

2023最新版本Camtasia电脑录屏软件好不好用?

在当今数字化时代&#xff0c;屏幕录制成为了许多用户制作教学视频、演示文稿、游戏攻略等内容的首选。本文将为您介绍几款常用的电脑录屏软件&#xff0c;包括Camtasia、OBS Studio、Bandicam等&#xff0c;并对其进行功能和用户体验方面的比较&#xff0c;同时给出10款电脑录…...

第三章 Linux 初步

第三章 Linux 初步 一、 基本操作 ①登录&#xff1a; Linux 是多用户系统&#xff0c;必须用正确的用户名和口令登录后才能 进入 Linux Shell 提示符状态。 默认的文本界面 Shell 提示符有两种&#xff1a; •root 用户登录后的提示符&#xff1a; # •普通用户登录后的…...

linux环境安装使用mysql详解

01-安装MySQL并启动 1.1 环境准备 # 1.卸载mariadb&#xff0c;否则安装mysql会出现冲突 &#xff08;1&#xff09;.执行命令rpm -qa | grep mariadb 会列出所有被安装的mariadb rpm 包&#xff1b; &#xff08;2&#xff09;.执行命令rpm -e --nodeps mariadb-libs-5.5.56…...

SUNTANS模型学习(9)——学习Tidal forcing算例

学习Tidal forcing算例 简介网格配置与地形定解条件设置初始条件设置边界条件设置开边界处的通量计算&#xff08;OpenBoundaryFluxes&#xff09;开边处的速度、水位&#xff08;BoundaryVelocities&#xff09; 其它参数配置模拟结果 简介 SUNTANS中 tidal forcing 算例的全…...

​力扣解法汇总1010. 总持续时间可被 60 整除的歌曲

目录链接&#xff1a; 力扣编程题-解法汇总_分享记录-CSDN博客 GitHub同步刷题项目&#xff1a; https://github.com/September26/java-algorithms 原题链接&#xff1a; 力扣 描述&#xff1a; 在歌曲列表中&#xff0c;第 i 首歌曲的持续时间为 time[i] 秒。 返回其总持…...

利用老毛桃pe启动U盘启动ubuntu.iso,完成ubuntu系统的安装

1.双U盘&#xff0c;一个是老毛桃pe启动盘&#xff0c;可以启动grub4dos&#xff0c;加载了run模块&#xff0c;很好用&#xff08;尤其是对不熟悉grub的小白&#xff09; 2.大容量U盘存放ubuntu-desktop-i386.iso&#xff0c;U盘的格式是ntfs格式&#xff08;其实这个不好&am…...

分享2个教学视频录制的方法!

案例&#xff1a;如何录制教学视频&#xff1f; 【我是一名老师&#xff0c;我想录制一些教学视频发布在网络平台上&#xff0c;但是我不知道如何操作。有没有人知道录制教学视频需要什么工具&#xff1f;如何录制&#xff1f;】 随着在线教育的普及&#xff0c;越来越多的教…...

「SQL面试题库」 No_63 报告的记录 II

&#x1f345; 1、专栏介绍 「SQL面试题库」是由 不是西红柿 发起&#xff0c;全员免费参与的SQL学习活动。我每天发布1道SQL面试真题&#xff0c;从简单到困难&#xff0c;涵盖所有SQL知识点&#xff0c;我敢保证只要做完这100道题&#xff0c;不仅能轻松搞定面试&#xff0…...

【事务】怎么去理解事务?

1、什么是事务&#xff1f; 事务是指作为单个逻辑工作单元执行的一系列操作&#xff0c;这些操作要么全做&#xff0c;要么全不做&#xff0c;是一个不可分割的工作单元。 2、事务具有哪些特性&#xff1f; 一个逻辑工作单元要成为事务&#xff0c;在关系型数据库管理系统中…...

camunda流程变量如何使用

Camunda是一个流程引擎&#xff0c;它支持在流程执行期间存储和操作流程变量。流程变量是一个值或对象&#xff0c;可以与Camunda中的流程实例、任务或执行相关联。 流程变量在Camunda中有很多用途。以下是一些常见的用途&#xff1a; 1、传递数据&#xff1a;流程变量可以用于…...

CMIP6:WRF模式动力降尺度、单点降尺度、统计方法区域降尺度

专题一 CMIP6中的模式比较计划 1.1 GCM介绍 ​ 1.2 相关比较计划介绍 ​ 专题二数据下载 2.1方法一&#xff1a;手动人工 ​ 利用官方网站 2.2方法二&#xff1a;自动 利用Python的命令行工具 ​ 2.3方法三&#xff1a;半自动购物车 利用官方网站 ​ 2.4 裁剪netCDF文件 …...

2023建筑设计师们有哪些好用的AI设计工具?

目前&#xff0c;建筑师要么单独工作&#xff0c;要么团队合作来完成设计过程&#xff0c;这可能需要数月甚至数年的时间。设计和准备用于开发的建筑物可能需要很长时间&#xff0c;有时甚至数年。一些比较繁琐的步骤可以自动化&#xff0c;但整个过程仍然需要大量的人工和时间…...

mysql主从复制与读写分离

mysql主从复制与读写分离 MySQL主从复制是一种常见的数据复制技术&#xff0c;用于将一个MySQL数据库服务器的数据复制到其他服务器上。 单台mysql在安全性&#xff0c;高并发方面都无法满足实际需求 配置多台主从数据库服务器以实现读写分离 读写分离&#xff0c;主数据库的…...

技术控,看这里,一款支持断点调试的数据科学工具

数据科学是一门利用统计学、机器学习、数据挖掘、数据可视化等技术和方法&#xff0c;从数据中提取知识和信息的交叉学科。自上世纪60年代&#xff0c;统计学家John W.Tukey首次提出“数据分析”&#xff08;Data Analysis&#xff09;的概念起&#xff0c;数据科学已历经了几十…...

论文导读 | 大语言模型上的精调策略

随着预训练语言模型规模的快速增长&#xff0c;在下游任务上精调模型的成本也随之快速增加。这种成本主要体现在两方面上&#xff1a;一&#xff0c;计算开销。以大语言模型作为基座&#xff0c;精调的显存占用和时间成本都成倍增加。随着模型规模扩大到10B以上&#xff0c;几乎…...

进阶自动化测试,这3点你一定要知道的...

自动化测试指软件测试的自动化&#xff0c;在预设状态下运行应用程序或系统&#xff0c;预设条件包括正常和异常&#xff0c;最后评估运行结果。将人为驱动的测试行为转化为机器执行的过程。 自动化测试框架一般可以分为两个层次&#xff0c;上层是管理整个自动化测试的开发&a…...

网络编程套接字API

一. linux平台 1.创建套接字 成功返回文件描述符&#xff0c;失败返回-1 int socket (int __domain, int __type, int __protocol) ;2.套接字绑定IP地址和端口号 成功返回0&#xff0c;失败返回-1 int bind (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len);3.开启…...

数字藏品的价值和意义

2022年以来&#xff0c;数字藏品概念在国内火热起来。从年初的《关于防范 NFT相关金融风险的倡议》到8月份央行数字货币 DCEP的正式面世&#xff0c;从中国香港首个“NFT”艺术品在香港拍卖市场成交到国内多家互联网大厂推出数字藏品平台&#xff0c;越来越多的企业开始试水数字…...

Unity物理系统脚本编程(上)

一、获取刚体组件Rigidbody 当一个物体挂载了刚体时&#xff0c;即可在脚本中获取该物体的刚体组件&#xff0c;代码如下 Rigidbody rigid; void Start() { rigidGetComponent<Rigidbody>(); } 一般将刚体变量命名为rigid并定义为一个字段&#xff0c;方便复用. 二、施…...

Java基础(十七)File类与IO流

1. java.io.File类的使用 1.1 概述 File类及本章下的各种流&#xff0c;都定义在java.io包下。一个File对象代表硬盘或网络中可能存在的一个文件或者文件目录&#xff08;俗称文件夹&#xff09;&#xff0c;与平台无关。&#xff08;体会万事万物皆对象&#xff09;File 能新…...

跑步课程导入能力,助力科学训练

HUAWEI Health Kit为开发者提供用户自定义的跑步课程导入接口&#xff0c;便于用户在华为运动健康App和华为智能穿戴设备上查看来自生态应用的训练课表&#xff0c;开启科学、适度的运动训练。 跑步课程导入能力支持生态应用在获取用户的华为帐号授权后&#xff0c;将跑步课程…...

MySQL---8、创建和管理表

1、基础知识 1.1 一条数据存储的过程 创建数据库-->确认字段-->创建数据表-->插入数据1.2 标识符的命名规则 1、数据库名、表名不得超过30个字符&#xff0c;变量名限制为29个 2、必须只能包含A-Z、a-z、0-9,、_共63个字符 3、数据库名、表名、字段名等对象名中间不…...

图像分类简单介绍

文章目录 图像分类简单介绍什么是图像分类图像分类的背景和意义传统的图像分类方法基于深度学习的图像分类方法总结 图像分类简单介绍 图像分类是计算机视觉领域的一个基本任务&#xff0c;其目标是将输入的图像分配给某个预定义的类别&#xff08;即标签&#xff09;。在本教…...

很多博主用Markdown格式文章?直呼真不错!

概述 Markdown 是一种轻量级标记语言&#xff0c;它可以使我们专注于写作内容&#xff0c;而不用过多关注排版&#xff0c;很多博主、作家等都用它来撰写文章~ 本文将给各位小伙伴介绍 Markdown 语法的使用&#xff0c;本篇文章索奇就是用的纯 markdown 语法来写的~ 标题 一级…...

【2023/05/07】汇编语言

Hello&#xff01;大家好&#xff0c;我是霜淮子&#xff0c;2023倒计时第2天。 Share Stray birds of summer come to my window to sing and fly away. And yellow leaves of autumn,which have no songs,flutter and full there with a sigh. 译文&#xff1a; 夏天的鸟&…...

AI 生成第3篇测试文章:怎么编写测试计划?

背景 在软件开发过程中&#xff0c;测试是十分重要的环节&#xff0c;测试计划是测试的基础和重要的组成部分。一个完善的测试计划能够指导测试工作&#xff0c;明确测试范围和要求&#xff0c;提高测试效率&#xff0c;保证软件质量和可靠性。本文将从测试计划的定义、编写步…...

怎么洗稿容易过稿-在线洗稿软件

自媒体洗稿软件 即使您是一位优秀的自媒体写作人员&#xff0c;也难免遇到让人头疼的撰写问题&#xff0c;例如无法处理大量原始文本、需要手动删除冗余信息、缺少时间针对每篇文章进行深入修改等问题。但是&#xff0c;现在有了我们的一款自媒体洗稿软件&#xff0c;您再也不需…...