【C++11】线程

本篇文章更多的是熟悉一下C++11的线程库接口,与linux的相关线程接口是非常相似的,更多的是将面向过程改为了面向对象。
并没有一些概念的讲解。
想知道线程的相关概念的可以看一看这篇文章及后续

在C++11之前,涉及到多线程问题,都是和平台相关的,
比如windows和linux下各有自己的接口,这使得代码的可移植性比较差。
C++11中最重要的特性就是对线程进行支持了,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含< thread >头文件。

目录

  • 线程接口:
    • 构造:
    • 常用接口:
    • 线程函数参数:
    • 具体实践:
      • 创建线程的方法:
      • 创建多线程的方法:
    • this_thread:
      • get_id:
      • yeild:
  • 互斥锁:
    • 锁的接口:
    • 锁的使用:
    • 其他的锁:
    • lock_guard:
    • unique_lock:
  • 条件变量:

线程接口:

构造:

首先我们来看构造,他有无参构造,带参构造(模板+万能引用+多参数模板),移动构造,却没有拷贝构造,
在这里插入图片描述
没有拷贝构造的原因在于在这里插入图片描述

常用接口:

在这里插入图片描述
注意:

  1. 线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的状态。
  2. 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。
#include <thread>
int main()
{std::thread t1;cout << t1.get_id() << endl;return 0;
}

get_id()的返回值类型为id类型,id类型实际为std::thread命名空间下封装的一个类,该类中包含了一个结构体:

// vs下查看
typedef struct
{ /* thread identifier for Win32 */void *_Hnd; /* Win32 HANDLE */unsigned int _Id;
} _Thrd_imp_t;
  1. 当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。
    线程函数一般情况下可按照以下三种方式提供:
    -函数指针
    -lambda表达式
    -仿函数
  2. thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不意向线程的执行。
  3. 可以通过jionable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效:
  • 采用无参构造函数构造的线程对象
  • 线程对象的状态已经转移给其他线程对象
  • 线程已经调用jion或者detach结束

线程函数参数:

线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此:即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。

void ThreadFunc1(int& x)
{x += 10;
}
void ThreadFunc2(int* x)
{*x += 10;
}
int main()
{int a = 10;// 在线程函数中对a修改,不会影响外部实参,因为:线程函数参数虽然是引用方式,但其实际引用的是线程栈中的拷贝thread t1(ThreadFunc1, a);t1.join();cout << a << endl;// 如果想要通过形参改变外部实参时,必须借助std::ref()函数thread t2(ThreadFunc1, std::ref(a);t2.join();cout << a << endl;// 地址的拷贝thread t3(ThreadFunc2, &a);t3.join();cout << a << endl;return 0;
}

那我们怎么理解多个线程执行同一个函数,因为每个线程都有独立的栈,所以虽然是同一个函数,却在不同的栈建立栈帧。

具体实践:

创建线程的方法:

由于可以使用lambda,我们选择直接使用lambda作为线程函数。

int main()
{thread t1([](int x)->void{for (int i = 0; i < x; i++){cout << " I am Thraed-1" << endl;}}, 10);thread t2([](int x)->void{for (int i = 0; i < x; i++){cout << " I am Thraed-2" << endl;}}, 20);t1.join();t2.join();return 0;
}

创建多线程的方法:

上段代码过于冗余,如果我们要创建更多但是功能相近的线程函数,该如何操作呢?
使用vector + thread无参构造即可轻松管理多线程。

在这里插入图片描述
注意:这里有一个细节,我们在循环体内应慎用lambda全部引用捕捉,如果上图代码采取引用捕捉,由于我们在lambda中使用了i这个for循环内部的控制变量,有可能还没有开始执行lambda,但是循环却已经结束,i到了5,导致数组越界从而报错。

this_thread:

为啥要看这个东西?因为很有用!
注意:这是std作用域中的this_thread域,里面有一些如下函数:
在这里插入图片描述

在这里插入图片描述
我们在创建线程时,如果需要有在lambda内打印id的需求,我们是没有办法打印出来的,即使我们的thread提供了get_id函数。在这里插入图片描述
这是因为t1对象还没有构建好。

那我们怎么获取?
就在这个命名空间内,有以上4个函数。

get_id:

在这里插入图片描述
这样即可在构建时得到对应的id。

yeild:

对于这个接口我们目前了解一下即可。

这是啥呢?
这是让出当前线程时间片的一个接口(对于OS的一个建议)。
例子:

假设你正在一个繁忙的办公室工作,你和同事都需要使用同一台打印机。为了公平起见,你们约定每个人每次只能使用打印机一分钟。现在,你正在打印一个文件,但你发现文件很长,一分钟内肯定打印不完。这时,你看到旁边有几位同事也在等着打印急件,他们看起来比你还着急。
于是,你决定做一个礼貌的举动:你暂时放下自己的打印任务,让出打印机给其他人使用。这就像是在说:“我还有很多页要打印,但我可以先等等,你们先用吧。”这就是std::this_thread::yield函数在多线程环境中的作用——它让当前线程“礼让”,给其他线程一个运行的机会。

一般在无锁编程(无锁编程指的是少用锁,因为效率低)中出现。那么这就要提一提CAS(cmpare_and_swap)了。

也就是原子操作。
比如我们的i++在汇编层面是至少3条语句:(拿出i,进行修改,放回内存)。
这就导致在多线程时出现线程安全,于是就衍生出了原子操作,这是由硬件直接支持好的,也就是只有一条汇编语句,不存在线程安全的问题,

对于线程安全来说,软件层面有很多方式,比如锁,原子操作…但是这些各种各样的方式都离不开硬件的支持。

这两种方法我们随后都会涉及到。

关于sleep_until是休眠到一个绝对时间,
而sleep_for是休眠你指定的时间,原理是比较复杂的,需要的时候直接看文档copy代码即可。

互斥锁:

当我们对一个全局变量i进行多线程++时,线程安全就毫无意外的出现了。

所以我们需要进行加锁。

锁的接口:

这里我们只关注常用的lock与unlock即可

锁是不可拷贝的。
在这里插入图片描述
注意,线程函数调用lock()时,可能会发生以下三种情况:

  • 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁
  • 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住
  • 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)

线程函数调用try_lock()时,可能会发生以下三种情况:

  • 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock释放互斥量
  • 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉
  • 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)

锁的使用:

注意放锁的位置即可。

int x = 0;
mutex mtx;
void func(int n)
{for (int i = 0; i < n; i++){mtx.lock();x++;mtx.unlock();}
}int main()
{size_t begin = clock();thread t1(func, 100000);thread t2(func, 500000);t1.join();t2.join();size_t end = clock();cout << x << endl;cout << end - begin << endl;return 0;
}

但是注意:
上下段两段代码哪个效率高?

void func(int n)
{mtx.lock();for (int i = 0; i < n; i++){x++;}mtx.unlock();
}

答案是下段高,为什么下段明明是串行却比上段的部分串行效率高?
在这里插入图片描述
这与我们学习到的要尽可能的细粒度加锁不太对劲啊。

原因有二:

  1. 频繁的lock与unlock
  2. i++太快了,当t1线程抢到锁,t2刚去阻塞,但是由于++太快了,导致刚去阻塞就又回来了,导致频繁的上下文切换。

其他的锁:

std::recursive_mutex

其允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权,释放互斥量时需要调用与该锁层次深度相同次数的 unlock(),除此之外,std::recursive_mutex 的特性和 std::mutex 大致相同。

std::timed_mutex

比 std::mutex 多了两个成员函数,try_lock_for(),try_lock_until() 。
for是锁你指定的时间,until是锁到你指定的时间。

std::recursive_timed_mutex

上述两个锁的特性相加。

lock_guard:

template<class _Mutex>
class lock_guard
{
public:// 在构造lock_gard时,_Mtx还没有被上锁explicit lock_guard(_Mutex& _Mtx): _MyMutex(_Mtx){_MyMutex.lock();}// 在构造lock_gard时,_Mtx已经被上锁,此处不需要再上锁lock_guard(_Mutex& _Mtx, adopt_lock_t): _MyMutex(_Mtx){}~lock_guard() _NOEXCEPT{_MyMutex.unlock();}lock_guard(const lock_guard&) = delete;lock_guard& operator=(const lock_guard&) = delete;
private:_Mutex& _MyMutex;
};

通过上述代码可以看到,lock_guard类模板主要是通过RAII的方式,对其管理的互斥量进行了封装,在需要加锁的地方,只需要用上述介绍的任意互斥体实例化一个lock_guard,调用构造函数成功上锁,出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁问题。

lock_guard的缺陷:太单一,用户没有办法对该锁进行控制,因此C++11又提供了unique_lock。

unique_lock:

与lock_gard类似,unique_lock类模板也是采用RAII的方式对锁进行了封装,并且也是以独占所有权的方式管理mutex对象的上锁和解锁操作,即其对象之间不能发生拷贝。

在构造(或移动(move)赋值)时,unique_lock 对象需要传递一个 Mutex 对象作为它的参数,新创建的unique_lock 对象负责传入的 Mutex 对象的上锁和解锁操作。使用以上类型互斥量实例化

unique_lock的对象时,自动调用构造函数上锁,unique_lock对象销毁时自动调用析构函数解锁,可以很方便的防止死锁问题。

与lock_guard不同的是,unique_lock更加的灵活,提供了更多的成员函数:

  • 上锁/解锁操作:lock、try_lock、try_lock_for、try_lock_until和unlock
  • 修改操作:移动赋值、交换(swap:与另一个unique_lock对象互换所管理的互斥量所有权)、释放(release:返回它所管理的互斥量对象的指针,并释放所有权)
  • 获取属性:owns_lock(返回当前对象是否上了锁)、operator bool()(与owns_lock()的功能相同)、mutex(返回当前unique_lock所管理的互斥量的指针)。

条件变量:

持续更新~

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

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

相关文章

访问控制系列

目录 一、基本概念 1.客体与主体 2.引用监控器与引用验证机制 3.安全策略与安全模型 4.安全内核 5.可信计算基 二、访问矩阵 三、访问控制策略 1.主体属性 2.客体属性 3.授权者组成 4.访问控制粒度 5.主体、客体状态 6.历史记录和上下文环境 7.数据内容 8.决策…

memcached 高性能内存对象缓存

memcached 高性能内存对象缓存 memcache是一款开源的高性能分布式内存对象缓存系统&#xff0c;常用于做大型动态web服务器的中间件缓存。 mamcached做web服务的中间缓存示意图 当web服务器接收到请求需要处理动态页面元素时&#xff0c;通常要去数据库调用数据&#xff0c;但…

【快速逆向一/无过程/有源码】《大学》在线投稿系统

逆向日期&#xff1a;2024.07.18 使用工具&#xff1a;Node.js 加密工具&#xff1a;Crypto-js标准库 文章全程已做去敏处理&#xff01;&#xff01;&#xff01; 【需要做的可联系我】 【点赞 收藏 关注 】仅供学习&#xff0c;仅供学习&#xff0c; 本文为快速逆向&#x…

如果制作红星照耀中国思维导图?6个软件帮助你快速制作思维导图

如果制作红星照耀中国思维导图&#xff1f;6个软件帮助你快速制作思维导图 制作《红星照耀中国》思维导图可以帮助更好地理解和梳理书中的重要信息和内容。以下是六款推荐的思维导图软件及其特点和使用方法&#xff0c;帮助你快速制作高质量的思维导图。 迅捷画图 特点与功…

Python基础语法篇(下)+ 数据可视化

Python基础语法&#xff08;下&#xff09; 数据可视化 一、函数&#xff08;一&#xff09;函数的定义&#xff08;二&#xff09;函数的调用和传参 二、文件操作&#xff08;一&#xff09;文件读取和写入&#xff08;二&#xff09;文件对象及方法&#xff08;三&#xff09…

【数学建模】——【线性规划】及其在资源优化中的应用

目录 线性规划问题的两类主要应用&#xff1a; 线性规划的数学模型的三要素&#xff1a; 线性规划的一般步骤&#xff1a; 例1&#xff1a; 人数选择 例2 &#xff1a;任务分配问题 例3: 饮食问题 线性规划模型 线性规划的模型一般可表示为 线性规划的模型标准型&…

达梦数据库的系统视图v$sqltext

达梦数据库的系统视图v$sqltext 在达梦数据库&#xff08;DM Database&#xff09;中&#xff0c;V$SQLTEXT 是一个系统视图&#xff0c;用于显示当前正在执行或最近执行的SQL语句的文本信息。这个视图对于监控和分析数据库中的SQL活动非常有用&#xff0c;尤其是在需要调试性…

【MySQL篇】Percona XtraBackup工具备份指南:常用备份命令详解与实践(第二篇,总共五篇)

&#x1f4ab;《博主介绍》&#xff1a;✨又是一天没白过&#xff0c;我是奈斯&#xff0c;DBA一名✨ &#x1f4ab;《擅长领域》&#xff1a;✌️擅长Oracle、MySQL、SQLserver、阿里云AnalyticDB for MySQL(分布式数据仓库)、Linux&#xff0c;也在扩展大数据方向的知识面✌️…

银河麒麟搭建ftp服务器

1.先 查看系统架构&#xff0c;我常遇到的一般银河麒麟是arrch64的 lscpu uname -a cat /etc/os-release 去下载对应版本的vsftp.rpm包和ftp包 Index of /NS/ (cs2c.com.cn) 1.安装rpm rpm -ivh *.rpm --nodeps --force #强制安装 2.修改配置文件 vi /etc/vsftpd/vsftpd.co…

PiT : 基于池化层Pooling layer的Vision Transformer

CNN的降维原理;随着深度的增加,传统CNN的通道维数增加,空间维数减少。经验表明,这样的空间降维对变压器结构也是有益的,并在原有的ViT模型的基础上提出了一种新的基于池的视觉变压器(PiT)。 1. 引言 ViT与卷积神经网络(CNN)有很大的不同。将输入图像分成1616小块馈送到变压…

LabVIEW软件开发的雷区在哪里?

在LabVIEW软件开发中&#xff0c;有几个需要注意的雷区&#xff0c;以避免常见的错误和提高开发效率&#xff1a; 1. 不良的代码结构 雷区&#xff1a;混乱的代码结构和不清晰的程序逻辑。 后果&#xff1a;导致难以维护和调试的代码&#xff0c;增加了错误和故障的风险。 …

Web3时代的教育技术革新:智能合约在学习管理中的应用

随着区块链技术的发展和普及&#xff0c;Web3时代正在为教育技术带来前所未有的革新和机遇。智能合约作为区块链技术的核心应用之一&#xff0c;不仅在金融和供应链管理等领域展示了其巨大的潜力&#xff0c;也在教育领域中逐渐探索和应用。本文将探讨智能合约在学习管理中的具…

【C++】前缀和:和为K的子数组

1.题目 2.算法 需要借助哈希表&#xff08;查找效率很高&#xff09;。 如果一个区间和为sum&#xff0c;如果它的前缀和为sum-k&#xff0c;那么后缀和一定是K。 3.代码

类与对象(3)

对于类的构造函数我们已经有了初步的了解&#xff0c;这里我们对其拷贝构造函数进行讲解&#xff1a; 目录 拷贝构造函数&#xff1a; 1.拷贝构造函数的作用&#xff1a; 2.系统生成拷贝构造函数的缺陷 3.深拷贝的实现 侧面体现 拷贝构造函数&#xff1a; 这里我们将拷贝…

小程序-模板与配置

一、WXML模板语法 1.数据绑定 2.事件绑定 什么是事件 小程序中常用的事件 事件对象的属性列表 target和currentTarget的区别 bindtap的语法格式 在事件处理函数中为data中的数据赋值 3.事件传参与数据同步 事件传参 &#xff08;以下为错误示例&#xff09; 以上两者的…

Java语言程序设计基础篇_编程练习题**14.29(游戏:豆机)

第十四章第二十九题 **14.29 (游戏&#xff1a;豆机) 请写一个程序&#xff0c;显示编程练习题 7.21 中介绍的豆机&#xff0c;如图 14-52c 所示 代码展示 package chapter_14;import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.layou…

NFS存储、API资源对象StorageClass、Ceph存储-搭建ceph集群和Ceph存储-在k8s里使用ceph(2024-07-16)

一、NFS存储 注意&#xff1a;在做本章节示例时&#xff0c;需要拿单独一台机器来部署NFS&#xff0c;具体步骤略。NFS作为常用的网络文件系统&#xff0c;在多机之间共享文件的场景下用途广泛&#xff0c;毕竟NFS配置方 便&#xff0c;而且稳定可靠。NFS同样也有一些缺点&…

Postgresql主键自增的方法

Postgresql主键自增的方法 一.方法&#xff08;一&#xff09; 使用 serial PRIMARY KEY 插入数据 二.方法&#xff08;二&#xff09; &#x1f388;边走、边悟&#x1f388;迟早会好 一.方法&#xff08;一&#xff09; 使用 serial PRIMARY KEY 建表语句如下&#xf…

住宅IP解析:动态住宅IP和静态住宅IP区别详解

在互联网连接的世界中&#xff0c;IP地址是我们识别和访问网络资源的关键。住宅IP地址&#xff0c;特别是动态住宅IP和静态住宅IP&#xff0c;是两种不同类型的IP分配方式&#xff0c;它们在使用和功能上存在显著差异。 1. IP地址的稳定性 动态住宅IP&#xff1a;这种IP地址是…

mysql命令练习

创建数据表grade: CREATE TABLE grade( id INT NOT NULL&#xff0c; sex CHAR(1)&#xff0c; firstname VARCHAR(20) NOT NULL&#xff0c; lastname VARCHAR(20) NOT NULL&#xff0c; english FLOAT&#xff0c; math FLOAT, chinese FLOAT )&#xff1b; 向数据表grade中插…