C++11---多线程

看前须知:如果对线程不了解的,可以先去看Linux---多线程(上),(下)这两篇文章

那里主要讲了线程的一些基础概念和底层相关理解,对我们阅读这篇文章会有所帮助

一、thread --- 线程

1、thread相关接口介绍

在C++11之前,涉及到多线程问题,都是和平台相关的,比如windows和linux下各有自己的接 口,这使得代码的可移植性比较差C++11中最重要的特性就是对线程进行支持了,使得C++在 并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含< thread >头文件
函数接口功能说明
thread()
构造一个线程对象,没有关联任何线程函数,即没有启动任何线程
thread(fn, args1, args2, ...)
构造一个线程对象,并关联线程函数fn,args1,args2,...为线程函数的参数
get_id()
获取线程id
joinable()
查看线程是否是连接状态,与detch后的线程的分离状态相对应
join()
该函数调用后会阻塞等待线程结束
如果线程是默认构造的线程对象 / 已经被detach / 已经被 join,再调用join会出错
detach()
在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离的线程变为后台线程,创建的线程的"死活"就与主线程无关

下面是一个简单的创建线程的代码

#include<thread>
#include<iostream>using namespace std;void Print(size_t n)
{for (size_t i = 0; i < n; i++){cout << i << " ";}cout << endl;
}int main()
{thread t(Print, 10);// t.detach();if (t.joinable())t.join();cout << t.joinable() << endl;return 0;
}

注意:

  • 线程是操作系统中的概念,线程对象可以关联一个线程,用来控制线程以及获取线程的状态
  • 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。
  • 当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。 线程函数一般情况下可按照以下三种方式提供: 函数指针lambda表达式、仿函数、包装器,如下
#include<iostream>
void Print(size_t n)
{cout << this_thread::get_id() << " : "; // 获取当前线程的线程idfor (size_t i = 0; i < n; i++){cout << i << " ";}cout << endl;
}struct print
{void operator()(size_t n){cout << this_thread::get_id() << " : ";for (size_t i = 0; i < n; i++){cout << i << " ";}cout << endl;}
};int main()
{thread t1(Print, 10);Sleep(1);thread t2(print(), 20);Sleep(1);thread t3([](size_t n) {cout << this_thread::get_id() << " : ";for (size_t i = 0; i < n; i++){cout << i << " ";}cout << endl;}, 30);Sleep(1);function<void(size_t)>f = [](size_t n) {cout << this_thread::get_id() << " : ";for (size_t i = 0; i < n; i++){cout << i << " ";}cout << endl;};thread t4(f, 40);t1.join();t2.join();t3.join();t4.join();return 0;
}
  • thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不影响线程的执行。

void Print(size_t n, string s)
{cout << s << " : ";for (size_t i = 0; i < n; i++){cout << i << " ";}cout << endl;
}int main()
{int n = 10;vector<thread> vthd(n);size_t j = 0;for (auto& thd : vthd){// 移动赋值 --- 临时变量是将亡值thd = thread(Print, 10, "线程" + to_string(j++));Sleep(1);// 休眠的目的是为了让打印出来的数据看起来不乱}for (auto& thd : vthd){thd.join();}thread t1(Print, 10, "zwxs");// thread t2(t1); // 错,thread不支持拷贝构造thread t2(move(t1)); //thread支持移动构造t2.join();return 0;
}

  • 可以通过jionable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效
  1. 采用无参构造函数构造的线程对象
  2. 线程对象的状态已经转移给其他线程对象
  3. 线程已经调用jion或者detach结束

2、线程函数参数

线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此,即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参,如果不理解,可以直接记住结论,即创建线程时传引用得加std::ref()

void Print(size_t n, int & x)
{for (size_t i = 0; i < n; i++){x++;}
}void Print1(size_t n, int* x)
{for (size_t i = 0; i < n; i++){(*x)++;}
}int main()
{int x = 0;// thread t(Print, 10, x); // 会报错thread t(Print, 10, ref(x)); // std::ref() 帮助我们传递引用t.join();cout << x << endl;thread t1(Print1, 10, &x); // 可以直接传指针,通过指针来修改值t1.join();cout << x << endl;return 0;
}

3、this_thread命名空间

this_thread 是 C++11 引入的一个命名空间,它位于 std 下,并提供了一组函数,用于操作当前执行的线程。这个命名空间的目的是为开发者提供一种便捷的方式来获取和管理当前线程的信息和行为。

  1. get_id:这个函数用于获取当前线程的线程 ID。线程 ID 是一个唯一标识线程的整数值,通过它可以在程序中区分和追踪不同的线程。
  2. yield:这个函数用于让当前线程主动放弃处理器的使用权,使得其他线程有机会执行。这是一种线程间的协作机制,有助于实现更高效的线程调度。
  3. sleep_for:这个函数使当前线程进入休眠状态,直到指定的时间段过去,这可以用于控制线程的执行节奏,或者在某些情况下,等待某些条件成立。
  4. sleep_until:这个函数使当前线程进入休眠状态,直到达到指定的时间点。它允许线程在特定的时间唤醒并执行。

与时间相关的函数可以结合<chrono>头文件下的相关函数使用,如hours,minutes,seconds等

void Print(size_t n)
{for (size_t i = 0; i < n; i++){cout << i << " ";this_thread::sleep_for(chrono::seconds(1));}
}

二、mutex --- 锁

在C++11中,Mutex总共包了四个互斥量的种类,都不支持拷贝构造/移动拷贝/赋值拷贝/移动赋值

1、std::mutex

C++11提供的最基本的互斥量,该类的对象之间不能拷贝,也不能进行移动。mutex最常用
的三个函数:
函数接口功能说明
lock()阻塞加锁
try_lock()非阻塞加锁
unlock()解锁

int main()
{int x = 0;int n = 1000;thread t1([&]() {for (int i = 0; i < n; i++){x++;}});thread t2([&]() {for (int i = 0; i < n; i++){x++;}});t1.join();t2.join();cout << x << endl;return 0;
}

很明显,上面的代码是线程不安全的,我们需要给线程加锁,代码如下

int main()
{int x = 0;int n = 1000;mutex mtx;thread t1([&]() {for (int i = 0; i < n; i++){mtx.lock();x++;mtx.unlock();}});thread t2([&]() {for (int i = 0; i < n; i++){mtx.lock();x++;mtx.unlock();}});t1.join();t2.join();cout << x << endl;return 0;
}

这里细心的读者可能已经发现了一个问题:为什么这里能"传"引用?注意:这里不是传参,而是lambda表达式的捕获列表,可以理解为两者底层走的不是一个逻辑,所以这里可以,至于具体底层是如何走的,有兴趣的可以自己去查查看。

2、std::recursive_mutex

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

3、std::timed_mutex

比 std::mutex 多了两个成员函数,try_lock_for(),try_lock_until() 。
try_lock_for()
接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与std::mutex 的 try_lock() 不同,try_lock 如果被调用时没有获得锁则直接返回 false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。
try_lock_until()
接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。

4、std::recursive_timed_mutex

具有recursive_mutex和timed_mutex的特性


出现死锁的情况,如下

void func(int x)
{if (x%2)throw exception("异常");elsecout << "func()" << endl;
}int main()
{mutex mtx;int y;thread t1([&]() {try {for (int i = 0; i < 10; i++) {mtx.lock();y++;func(i);mtx.unlock();}}catch (const exception& e) {cout << e.what() << endl;}});thread t2([&]() {try {for (int i = 0; i < 10; i++) {mtx.lock();y++;func(i);mtx.unlock();}}catch (const exception& e) {cout << e.what() << endl;}});t1.join();t2.join();return 0;
}

针对上面的情况,我们需要有一个能自动释放的锁,类似智能指针,库给我们提供了lock_guard和unique_lock用来封装锁,不需要我们去手动释放锁。

thread t1([&]() {try {for (int i = 0; i < 10; i++) {lock_guard<mutex> lock(mtx);y++;func(i);}}catch (const exception& e) {cout << e.what() << endl;}
});

lock_guard和unique_lock的区别:

lock_guard只支持构造和析构,没有其他功能

unique_lock能支持手动的加锁和解锁,并且能用时间进行控制

三、condition_variable --- 条件变量

不了解的可以先去看Linux---多线程(下)

常用的三个函数接口:

函数接口功能说明
void wait(unique_lock<mutex>& lck)等待条件就绪,再往下执行,会先释放申请到的锁,故只能传unique_lock,lock_guard不支持手动加锁和解锁
void notify_one()唤醒在条件变量的等待队列中的一个线程,需要重新加锁
void notify_all()唤醒在条件变量的等待队列中的所有线程,需要重新加锁
如何实现两个线程交替打印,一个打印奇数,一个打印偶数?
int main()
{int x = 1;condition_variable cv;mutex mtx;thread t1([&]() {for (int i = 0; i < 10; i++) {unique_lock<mutex> lock(mtx);while (x%2==0)cv.wait(lock);cout << this_thread::get_id() << " : " << x++ << endl;cv.notify_one();}});thread t2([&]() {for (int i = 0; i < 10; i++) {unique_lock<mutex> lock(mtx);while (x%2)cv.wait(lock);cout << this_thread::get_id() << " : " << x++ << endl;cv.notify_one();}});t1.join();t2.join();return 0;
}

四、atomic

C++11中的<atomic>库提供了对原子操作的支持,这些操作在多线程环境中是线程安全的。原子操作是不可中断的操作,即在执行完毕之前不会被其他线程打断。通过使用原子操作,我们可以避免使用互斥量(mutexes)和条件变量(condition variables)等同步原语,从而在某些情况下提高性能并简化代码

比如上面用锁保证x++的线程安全,其实比较浪费资源,因为申请锁一旦失败,线程就会阻塞,需要让出cpu,切换上下文数据等,而我们加锁只是为了执行x++这一条语句,显然很不值得,这里我们就可以用atomic中的函数,进行原子操作,不需要进入阻塞,代码如下

int main()
{atomic<int> x = 0;int n = 1000;//mutex mtx;thread t1([&]() {for (int i = 0; i < n; i++){x++;// 这里的++用的是运算符重载}});thread t2([&]() {for (int i = 0; i < n; i++){x++;// 这里的++用的是运算符重载}});t1.join();t2.join();std::cout << x << std::endl;return 0;
}

可以简单说明一下,它用的无锁编程---利用原子操作实现锁的功能。上面的++操作符就是用CAS (compare and swap)这个原子操作实现的(CAS在不同的语言中都会有对应的函数)

/*伪代码
int x = 0;
int old,newval;
do
{old = x;newval = old + 1;
}while(!CAS(&x,&old,newval)) 
//看x在内存中的值是否和old相同,如果相同*x=newval,返回true,否则返回false
*/

如果对无锁编程感兴趣可以去查查文档,这里就不多做介绍了,这个对编程能力要求有点高,建议最好不要轻易去写。

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

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

相关文章

第7期 部署两地三中心解决方案SDRS+CBR

第7期 部署两地三中心解决方案SDRSCBR 1.实施步骤&#xff08;部署跨可用区容灾&#xff09;配置跨可用区容灾操作场景约束与限制创建保护组 什么是SDRS什么是CBR什么是两地三中心容灾方案&#xff08;SDRSCBR&#xff09;应用场景方案优势三种容灾方案对比 2.两地三中心方案原…

边缘计算【智能+安全检测】系列教程--使用OpenCV+GStreamer实现真正的硬解码,完全消除马赛克

通过现有博客的GST_URL = "rtspsrc location=rtsp://admin:abcd1234@192.168.1.64:554/h264/ch01/main/av_stream latency=150 ! rtph264depay ! avdec_h264 ! videorate ! videoconvert ! appsink sync=false" GStreamer的解码方式解码,大多情况应该存在上图马赛克…

C/C++ BM25二叉树的后序遍历

文章目录 前言题目解决方案一1.1 思路阐述1.2 源码 解决方案二总结 前言 和前序遍历中序遍历一致&#xff0c;换个顺序就行。 详情参考C/C BM23 二叉树的前序遍历 详情参考C/C BM24 二叉树的中序遍历 题目 给定一个二叉树&#xff0c;返回他的后序遍历的序列。 后序遍历是值…

etcd相关知识整理归纳 —— 筑梦之路

什么是etcd? Etcd 是 CoreOS 团队于2013年6月发起的开源项目&#xff0c;它的目标是构建一个高可用的分布式键值(key-value)数据库。etcd内部采用raft协议作为一致性算法&#xff0c;Etcd基于 Go 语言实现。 名字由来&#xff0c;它源于两个方面&#xff0c;unix的“/etc”文件…

Mathorcup 甲骨文识别

本资源主要包含第2-4问&#xff0c;第一问直接使用传统图像处理即可&#xff0c;需要有很多步骤&#xff0c;这一步大家自己写就行。 2 第2问&#xff0c;甲骨文识别 2.1 先处理源文件 原文件有jpg和json文件&#xff0c;都在一个文件夹下&#xff0c;需要对json文件进行处理…

Windows环境下删除MySQL

文章目录 一、关闭MySQL服务1、winR打开运行&#xff0c;输入services.msc回车2、服务里找到MySQL并停止 二、卸载MySQL软件1、打开控制模板--卸载程序--卸载MySQL相关的所有组件 三、删除MySQL在物理硬盘上的所有文件1、删除MySQL的安装目录&#xff08;默认在C盘下的Program …

各省份自然灾害损失情况数据集(2004-2022年)

01、数据简介 自然灾害是指给人类生存带来危害或损害人类生活环境的自然现象&#xff0c;这些现象是地球演化过程的自然现象。它们主要包括气象灾害、地质灾害、海洋灾害、生物灾害、森林草原火灾等五大类。 具体来说&#xff0c;气象灾害包括干旱、洪涝灾害、台风、风雹、低…

每日OJ题_01背包④_力扣1049. 最后一块石头的重量 II

目录 力扣1049. 最后一块石头的重量 II 问题解析 解析代码 滚动数组优化代码 力扣1049. 最后一块石头的重量 II 1049. 最后一块石头的重量 II 有一堆石头&#xff0c;用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。 每一回合&#xff0c;从中选出任意…

斐尔玫瑰荣获《中国3.15诚信企业》证书,诚信经营赢得社会认可

2024年&#xff0c;斐尔玫瑰&#xff0c;荣获了备受瞩目的《中国3.15诚信企业》证书。这一荣誉的获得&#xff0c;不仅是对斐尔玫瑰长期以来坚持诚信经营、提供优质产品和服务的肯定&#xff0c;更是对其在消费者心目中建立起的良好信誉和口碑的认可。 斐尔玫瑰作为女性私密护…

CSS盒模型(详讲)

目录 概述&#xff1a; 内容区&#xff08;content&#xff09;&#xff1a; 内边距&#xff08;paddingj&#xff09;&#xff1a; 前言&#xff1a; 设置内边距&#xff1a; 边框&#xff08;border&#xff09;&#xff1a; 前言&#xff1a; 示例&#xff1a; 外边…

vivado ila 运行触发器、停止触发器、使用自动重新触发

运行触发器 您可在 2 种不同模式下运行或装备 ILA 核触发器 &#xff1a; • “ Run Trigger ” &#xff1a; 选择要装备的 ILA 核 &#xff0c; 然后单击“ ILA 仪表板 (ILA Dashboard) ”窗口或“硬件 (Hardware) ”窗口 工具栏上的“ Run Trigger ”按钮即可装备 IL…

JVM、maven、Nexus

一、jvm简介 1.应用程序申请内存时出现的三种情况&#xff1a; ①OOM:内存溢出&#xff0c;是指应用系统中存在无法回收的内存或使用的内存过多&#xff0c;最终使得程序运行要用到的内存大于能提供的最大内存。此时程序就运行不了&#xff0c;系统会提示内存溢出&#xff0c…