【C++11】多线程

      • 多线程
        • 创建线程
        • thread提供的成员函数
        • 获取线程id的方式
        • 线程函数参数的问题
        • 线程join场景和detach
      • 互斥量库(mutex)
        • mutex
        • recursive_mutex
        • lock_guard 和 unique_lock
      • 原子性操作库(atomic)
      • 条件变量库(condition_varuable)
      • 综合案例(实现两个线程交替打印1-100)

多线程

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

创建线程

在这里插入图片描述

  1. 调用无参的构造函数创建

thread提供了无参的构造函数,调用无参的构造函数创建出来的线程对象没有关联任何的线程函数对象,也没有启动任何线程。

thread t1;
#include <iostream>
#include <thread>
using namespace std;void func(int n)
{cout << n << endl;
}int main()
{thread t1;t1 = thread(func, 10);t1.join();return 0;
}

我们的thread是提供了移动赋值函数的,所以,当后序需要让该线程关联线程函数的时候,我们可以定义一个匿名的线程,然后调用移动赋值传给他
在这里插入图片描述
thread类是防拷贝的,不允许拷贝构造和拷贝赋值,但是可以移动构造和移动赋值,可以将一个线程对象关联线程的状态转移给其他线程对象,并且转移期间不影响线程的执行。

  1. 调用带参的构造函数

thread的带参构造函数的定义如下:

template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);

参数说明:

  • fn:可调用对象,比如:仿函数,指针,lambda表达式,被包装器包装后的可调用对象等。
  • args:进行对fn进行传值。

调用带参的构造函数创建线程对象,能够将线程对象与线程函数fn进行关联。比如:

void Func(int n, int num)
{for (int i = 0; i < n; i++){cout <<num<<":" << i << endl;}cout << endl;
}int main()
{thread t2(Func, 10,666);t2.join();return 0;
}

在这里插入图片描述

结合之前的lambda函数,这么我们可以很明显的看到lambda的作用

int main()
{//thread t2(Func, 10,666);thread t2([](int n = 10, int num = 666){for (int i = 0; i < n; i++){cout << num << ":" << i << endl;}cout << endl;});t2.join();return 0;
}

其输出结果和上图是一样的,注意,这么线程的形参只有一个仿函数lambda

使用一个容器来保存线程

#include <iostream>
#include <thread>
#include <vector>
using namespace std;
int main()
{size_t m;//线程个数cin >> m;vector<thread> vthread(m);//直接初始化长度为10for (int i = 0; i < m; ++i){vthread[i] = thread([i](){cout << "我是第" << i << "个线程" << endl;});}for (auto& e : vthread){e.join();}return 0;
}

在这里插入图片描述

thread提供的成员函数

在我们多线程中,常用的成员函数如下:

  • join:对该线程进行等待,在等待的线程返回之前,调用join将会将线程进行阻塞,我们主要阻塞的是主线程。
  • joinable:判断该线程是否已经执行完毕,如果是则返回true,否则返回false。
  • 在这里插入图片描述
  • detach:将该线程进行分离主线程,被分离后,不在需要创建线程的主线程调用join进行对其等待
  • get_id:获取该线程的id

此外,joinable函数还可以用于判定线程是否是有效的,如果是以下任意情况,则线程无效:

  • 采用无参构造函数构造的线程对象。(该线程对象没有关联任何线程)
  • 线程对象的状态已经转移给其他线程对象。(已经将线程交给其他线程对象管理)
  • 线程已经调用join或detach结束。(线程已经结束)
获取线程id的方式

其实调用线程id的方法有两种,实际情况我们看下边的代码:

#include <iostream>
#include <thread>
#include <vector>
using namespace std;
int main()
{size_t m;//线程个数cin >> m;vector<thread> vthread(m);//直接初始化长度为10for (int i = 0; i < m; ++i){vthread[i] = thread([i](){printf("我是第%d个线程\n", i);cout << this_thread::get_id() << endl;});}for (auto& e : vthread){cout << e.get_id() << endl;}for (auto& e : vthread){e.join();}return 0;
}

在这里插入图片描述

  • 两者调用ID的环境是不同的
  • 从代码中我们可以看到get_id是需要线程对象来调用的
  • 但是this_thread::get_id我们通过多线程提供的特殊窗口可以不通过线程对象就可以直接调用

this_thread命名空间中还提供了以下三个函数:

  • yield:当前线程“放弃”执行,让操作系统调度另一线程继续执行
  • sleep_until :让当前线程休眠到一个具体时间点
  • sleep_for:让当前线程休眠一个时间段
线程函数参数的问题

线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,就算线程函数的参数为引用类型,在线程函数中修改后也不会影响到外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。比如:

void add(int& num)
{num++;
}
int main()
{int num = 0;thread t(add, num);t.join();cout << num << endl; //0return 0;
}

解决其办法有三种:

  • 方式一:借助std::ref函数

  • 在这里插入图片描述

  • 方式二:地址的拷贝

  • 在这里插入图片描述

  • 方式三:借助lambda表达式

  • 在这里插入图片描述

线程join场景和detach

当启动线程后,如果不使用join进行阻塞等待的话程序就会直接报错,因为此时存在内存泄漏问题。

thread库给我们提供了一共两种回收线程的方法

join方式

使用join进行线程回收时,一个线程只能回收一个,如果进行多次回收就会直接报错

但是如果你对一个线程回收后,又对此线程再一次的进行了移动赋值,那么此时还可以再一次的对线程进行二次join,如下代码案例:

void func(int n = 20)
{cout << n << endl;
}int main()
{thread t(func, 20);t.join();t = thread([](int num = 10) {cout << num << endl; });t.join();
}

在这里插入图片描述
需要注意的是,如果线程运行起来后,线程的内部发生了异常,那么我们锁设置的阻塞就起不到任何作用了,

detach方式

主线程创建新线程后,也可以调用detach进行将线程和主线程分离,分离后,新线程会到后台运行,其所有权和控制权将会交给C++运行库,此时,c++运行库会保证当线程退出时,其相关资源能够被正确回收

#include <mutex>
#include <iostream>
#include <thread>
int x = 0;
mutex mtx;
void threadFunc(int n)
{mtx.lock();for (int i = 0; i < 100000; i++){x++;}std::cout << "Hello from detached thread:" << n << std::endl;mtx.unlock();
}int main()
{std::thread t1(threadFunc,11111);std::thread t2(threadFunc,22222);t1.detach();  // 将线程设置为可分离的t2.detach();  // 将线程设置为可分离的// 主线程继续执行其他任务std::cout << "Main thread continues..." << std::endl;// 不要忘记在主线程结束前等待一段时间,以免分离的线程还未执行完std::this_thread::sleep_for(std::chrono::seconds(1));cout << x << endl;return 0;
}

在这里插入图片描述

互斥量库(mutex)

在我们的c++11中,我们一共提供了四种互斥锁形式

mutex

1. std::mutex
mutex锁是C++11提供的最基本的互斥量,mutex对象之间不可以进行拷贝,也不能进行移动。

mutex中常用的成员函数如下:

  • lock:对互斥量进行加锁
  • unlock:对互斥量进行解锁,释放互斥量所有权
  • try_lock:尝试对互斥量进行加锁。

线程函数调用lock时,可能会发生三种情况:

  • 如果互斥量当前没有被其他线程锁住,则调用线程将该互斥量进行加锁,知道调用 unlock后,才对其进行解锁。
  • 如果进行加锁时,发现已经被其他线程已经加过锁了,此时会进入阻塞状态。
  • 如果该互斥量被当前调用线程锁住,则会产生死锁

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

  • 如果互斥量当前没有被其他线程锁住,则调用线程将该互斥量进行加锁,知道调用 unlock后,才对其进行解锁。
  • 如果进行加锁时,发现已经被其他线程已经加过锁了,此时会返回false,并不会对其进行阻塞。
  • 如果该互斥量被当前调用线程锁住,则会产生死锁

在没有对临界资源加锁的时候,由于是多个进程同时进行,这时,不能同步的,正确的完成我们的任务,此时我们就需要给临界资源进行加锁

在这里插入图片描述

正确做法

#include <mutex>
#include <iostream>
#include <thread>
int x = 0;
mutex mtx;
void threadFunc(int n)
{mtx.lock();for (int i = 0; i < 100000; i++){x++;}std::cout << "Hello from detached thread:" << n << std::endl;mtx.unlock();
}int main()
{std::thread t1(threadFunc,11111);std::thread t2(threadFunc,22222);t1.detach();  // 将线程设置为可分离的t2.detach();  // 将线程设置为可分离的// 主线程继续执行其他任务std::cout << "Main thread continues..." << std::endl;// 不要忘记在主线程结束前等待一段时间,以免分离的线程还未执行完std::this_thread::sleep_for(std::chrono::seconds(1));cout << x << endl;return 0;
}

在这里插入图片描述

recursive_mutex

2. std::recursive_mutex
recursive_mutex叫做递归互斥锁,该锁专门用于递归函数中的加锁操作。

  • 有一些线程避免不了它是递归函数,但是如果对递归里的临界资源进行加锁时,可能会持续申请自己还还未释放的锁,进而导致死锁问题。
  • 而recursive_mutex允许同一个线程对互斥量进行多次上锁(即递归上锁),来获得互斥量对象的多层所有权,但是释放互斥量时需要调用与该锁层次深度相同次数的unlock。
int x = 0;
recursive_mutex mtx;void Func(int n)
{if (n == 0)return;//递归锁的原理就是当调用自己本身时,发现给自己上锁的还是自己,这时会自动解锁,通过此种方法来进行重复加锁解锁mtx.lock();++x;Func(n - 1);mtx.unlock();
}int main()
{thread t1(Func, 1000);thread t2(Func, 2000);t1.join();t2.join();cout << x << endl;return 0;
}

在这里插入图片描述
后两种可以查看收藏

lock_guard 和 unique_lock

为什么要使用lock_guard 和 unique_lock呢?

在我们平时使用锁中,如果锁的范围比较大,那么极度有可能在中途时忘记解锁,此后申请这个锁的线程就会被阻塞住,也就造成了死锁的问题,例如:

mutex mtx;
void func()
{mtx.lock();//...FILE* fout = fopen("data.txt", "r");if (fout == nullptr){//...return; //中途返回(未解锁)}//...mtx.unlock();
}
int main()
{func();return 0;
}

因此使用互斥锁时如果控制不好就会造成死锁,最常见的就是此处在锁中间代码返回,此外还有一个比较常见的情况就是在锁的范围内抛异常,也很容易导致死锁问题。

因此C++11采用RAII的方式对锁进行了封装,于是就出现了lock_guard和unique_lock。

为此c++11推出了解决方案,采用RAII的方式对锁进行了封装,于是就出现了lock_guard和unique_lock。

lock_guard

lock_guard是C++11中的一个模板类,其定义如下:

template <class Mutex>
class lock_guard;

通过这种构造对象时加锁,析构对象时自动解锁的方式就有效的避免了死锁问题。比如:

mutex mtx;
void func()
{lock_guard<mutex> lg(mtx); //调用构造函数加锁//...FILE* fout = fopen("data.txt", "r");if (fout == nullptr){//...return; //调用析构函数解锁}//...
} //调用析构函数解锁
int main()
{func();return 0;
}

模拟实现lock_guard

模拟实现lock_guard类的步骤如下:

  • lock_guard类中包含一个锁成员变量(引用类型),这个锁就是每个lock_guard对象管理的互斥锁。
  • 调用lock_guard的构造函数时需要传入一个被管理互斥锁,用该互斥锁来初始化锁成员变量后,调用互斥锁的lock函数进行加锁。
  • lock_guard的析构函数中调用互斥锁的unlock进行解锁。
  • 需要删除lock_guard类的拷贝构造和拷贝赋值,因为lock_guard类中的锁成员变量本身也是不支持拷贝的
class Lock_guard
{
public:Lock_guard(mutex& mtx):_mtx(mtx){_mtx.lock(); //加锁}~Lock_guard(){_mtx.unlock();//解锁}Lock_guard(const Lock_guard&) = delete;Lock_guard& operator=(const Lock_guard&) = delete;
private:mutex& _mtx;
};mutex mtx;
int x;
int main()
{thread t1([](){Lock_guard lg(mtx);x = 1;});t1.join();cout << x << endl;
}

在这里插入图片描述

unique_lock

比起lock_guard来说,unique_lock的逻辑和lock_guard的逻辑是一样的,都是调用类时加锁,出作用域时解锁,但是unique_lock比起lock_guard多加了几个条件,分别是:

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

具体实现逻辑,我们看下边的交替打印100的案例。

原子性操作库(atomic)

在上面的加锁案例中,多个线程同时对全局变量x进行++操作,并且,我们还对此操作进行了加锁,主要原因时++操作不是原子的,原子究竟是个什么呢?
原子大概的来说只有两个状态,一种是在运行,一种是不在运行,当处于在运行状态时,当其他进程进来时,发现为在运行状态就会进行等待, 知道状态模式换了才会进入下一个线程。

在这里插入图片描述

int main()
{int n = 100000;atomic<int> x = 0;//atomic<int> x = {0};//atomic<int> x{0};//int x = 0;mutex mtx;size_t begin = clock();thread t1([&, n](){for (int i = 0; i < n; i++){++x;}});thread t2([&, n]() {for (int i = 0; i < n; i++){++x;}});t1.join();t2.join();cout << x << endl;return 0;
}

在这里插入图片描述

条件变量库(condition_varuable)

condition_variable中提供的成员函数,可分为wait系列和notify系列两类。

wait系列成员函数(wait自带解锁功能和阻塞功能)

wait系列成员函数的作用就是让调用线程进行排队阻塞等待,包括waitwait_forwait_until

下面先以wait为例子,wait函数提供了两个不同版本的接口:

//版本一
void wait(unique_lock<mutex>& lck);
//版本二
template<class Predicate>
void wait(unique_lock<mutex>& lck, Predicate pred);

函数说明:

  • 调用第一个版本的时候,需要传入的参数只有一个互斥锁,线程调用wait后,就会进入阻塞状态,直到被唤醒
  • 调用第二个版本的时候,不仅要传入一个互斥锁,还需要传入一个返回值类型为bool的可调用对象,与第一个版本的wait不同的是,当线程被唤醒后还需要调用传入的可调用对象,如果可调用对象的返回值为false,那么该线程还需要继续被阻塞。

实际wait具有两个功能

  • 一个是让线程在条件不满足时进行阻塞等待,另一个是让线程将对应的互斥锁进行解锁。
  • 当线程被阻塞时这个互斥锁会被自动解锁,而当这个线程被唤醒时,又会自动获得这个互斥锁。

notify系列成员函数

notify系列成员函数的作用就是唤醒等待的线程,包括notify_one和notify_all。

  • notify_one:唤醒等待队列中的首个线程,如果等待队列为空则什么也不做。
  • notify_all:唤醒等待队列中的所有线程,如果等待队列为空则什么也不做。

综合案例(实现两个线程交替打印1-100)

尝试用两个线程交替打印1-100的数字,要求一个线程打印奇数,另一个线程打印偶数,并且打印数字从小到大依次递增。

该题目主要考察的就是线程的同步和互斥。

#include<mutex>
#include<condition_variable>int main()
{mutex mtx;condition_variable cv;int n = 100;int x = 1;// 问题1:如何保证t1先运行,t2阻塞?// 问题2:如何防止一个线程不断运行?thread t1([&, n]() {while (1){unique_lock<mutex> lock(mtx);if (x >= 100)break;//if (x % 2 == 0) // 偶数就阻塞//{//	cv.wait(lock);//}cv.wait(lock, [&x]() {return x % 2 != 0; });cout << this_thread::get_id() << ":" << x << endl;++x;cv.notify_one();}});thread t2([&, n]() {while (1){unique_lock<mutex> lock(mtx);if (x > 100)break;//if (x % 2 != 0) // 奇数就阻塞//{//	cv.wait(lock);//}cv.wait(lock, [&x](){return x % 2 == 0; });cout << this_thread::get_id() << ":" << x << endl;++x;cv.notify_one();}});t1.join();t2.join();return 0;
}

在这里插入图片描述

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

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

相关文章

【2】c++设计模式——>UML表示类之间的继承关系

继承也叫作泛化&#xff08;Generalization&#xff09;&#xff0c;用于描述父子类之间的关系&#xff0c;父类又称为基类或者超类&#xff0c;子类又称作派生类。在UML中&#xff0c;继承关系用带空心三角形的实线来表示。 关于继承关系一共有两种&#xff1a;普通继承关系和…

操作系统学习笔记1

文章目录 1、OS的一个宏观比喻2、OS的目的和功能3、OS的发展4、OS的运行机制5、OS的特征6、OS的体系结构 参考视频&#xff1a;操作系统 1、OS的一个宏观比喻 2、OS的目的和功能 3、OS的发展 4、OS的运行机制 中断、系统调用、异常。 5、OS的特征 6、OS的体系结构

docker--redis容器部署及地理空间API的使用示例-II

文章目录 Redis 地理位置类型API命令操作示例JAVA使用示例导入依赖RedisTemplate 操作GeoData示例CityInfo实体类Geo操作接口类Geo操作接口实现类SpringBoot测试类RedissonClient 操作GeoData示例docker–redis容器部署及与SpringBoot整合 docker–redis容器部署及地理空间API的…

解决ASP.NET Core的中间件无法读取Response.Body的问题

概要 本文主要介绍如何在ASP.NET Core的中间件中&#xff0c;读取Response.Body的方法&#xff0c;以便于我们实现更多的定制化开发。本文介绍的方法适用于.Net 3.1 和 .Net 6。 代码和实现 现象解释 首先我们尝试在自定义中间件中直接读取Response.Body&#xff0c;代码如…

【算法训练-二分查找 三】【特殊二分】寻找峰值

废话不多说&#xff0c;喊一句号子鼓励自己&#xff1a;程序员永不失业&#xff0c;程序员走向架构&#xff01;本篇Blog的主题是【数组的二分查找】&#xff0c;使用【数组】这个基本的数据结构来实现&#xff0c;这个高频题的站点是&#xff1a;CodeTop&#xff0c;筛选条件为…

计组—— I/O系统

&#x1f4d5;&#xff1a;参考王道课件 目录 一、I/O系统的基本概念 1.什么是“I/O”&#xff1f; ​编辑2.主机如何和I/O设备进行交互&#xff1f; 3.I/O控制方式 &#xff08;1&#xff09;程序查询方式 &#xff08;2&#xff09;程序中断方式 &#xff08;3&#x…

CSS详细基础(一)选择器基础

本帖开始&#xff0c;我们来介绍CSS——即&#xff0c;层叠样式表~ 层叠样式表是一种用来表现HTML&#xff08;标准通用标记语言的一个应用&#xff09;或XML&#xff08;标准通用标记语言的一个子集&#xff09;等文件样式的计算机语言。简单的说&#xff0c;层叠就是对一个元…

Zama的fhEVM:基于全同态加密实现的隐私智能合约

1. 引言 Zama的fhEVM定位为&#xff1a; 基于全同态加密实现的隐私智能合约 解决方案 开源代码见&#xff1a; https://github.com/zama-ai/fhevm&#xff08;TypeScript Solidity&#xff09; Zama的fhEVM协议中主要包含&#xff1a; https://github.com/zama-ai/tfhe-…

2023/10/4 -- ARM

今日任务&#xff1a;QT实现TCP服务器客户端搭建的代码&#xff0c;现象 ser&#xff1a; #include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this);server new QTcpSe…

多线程(pthread库)

POSIX线程库 引言 前面我们提到了Linux中并无真正意义上的线程 从OS角度来看&#xff0c;这意味着它并不会提供直接创建线程的系统调用&#xff0c;它最多给我们提供创建轻量级进程LWP的接口 但是从用户的角度来看&#xff0c;用户只认识线程啊&#xff01; 因此&#xff0c;…

十天学完基础数据结构-第二天(数据结构简介)

什么是数据结构&#xff1f; 在计算机科学中&#xff0c;数据结构是一种组织和存储数据的方式。它定义了数据的布局&#xff0c;以及对这些数据执行的操作。你可以把数据结构看作是计算机内存中的特定组织方式&#xff0c;就像图书馆中书籍的排列一样。 数据结构可以是各种形…

C++算法 —— 动态规划(9)完全背包问题

文章目录 1、动规思路简介2、完全背包【模板】3、零钱兑换4、零钱兑换Ⅱ5、完全平方数 背包问题需要读者先明白动态规划是什么&#xff0c;理解动规的思路&#xff0c;并不能给刚接触动规的人学习。所以最好是看了之前的动规博客&#xff0c;以及01背包博客&#xff0c;才能看完…

Vue11 计算属性

先看不用计算属性的两种方法 插值语法 简单的计算&#xff0c;可以使用插值语法 <!DOCTYPE html> <html><head><meta charset"UTF-8" /><title>姓名案例_插值语法实现</title><!-- 引入Vue --><script type"te…

软件测试基础学习

注意&#xff1a; 各位同学们&#xff0c;今年本人求职目前遇到的情况大体是这样了&#xff0c;开发太卷&#xff0c;学历高的话优势非常的大&#xff0c;公司会根据实际情况考虑是否值得培养&#xff08;哪怕技术差一点&#xff09;&#xff1b;学历稍微低一些但是技术熟练的…

MATLAB算法实战应用案例精讲-【优化算法】A*算法

前言 A*算法最早于1964年在IEEE Transactions on Systems Science and Cybernetics中的论文《A Formal Basis for the Heuristic Determination of Minimum Cost Paths》中首次提出。 其属于一种经典的启发式搜索方法,所谓启发式搜索,就在于当前搜索结点往下选择下一步结点时…

SpringMVC处理请求核心流程

一、前言 SpringMVC是一个基于Java的Web框架&#xff0c;它使用MVC&#xff08;Model-View-Controller&#xff09;设计模式来处理Web请求。在SpringMVC中&#xff0c;请求处理的核心流程主要包括以下几个步骤&#xff1a; 1、用户发送请求到前端控制器&#xff08;Dispatche…

Linux:minishell

目录 1.实现逻辑 2.代码及效果展示 1.打印字符串提示用户输入指令 2.父进程拆解指令 3.子进程执行指令,父进程等待结果 4.效果 3.实现过程中遇到的问题 1.打印字符串的时候不显示 2.多换了一行 3.cd路径无效 4.优化 1.ll指令 2.给文件或目录加上颜色 代码链接 模…

k8s集群的简单搭建

K8S简单集群搭建 前提条件 windos11电脑&#xff0c;内存16g以上安装vmware虚拟机软件安装三个centos7虚拟机&#xff0c;分配硬盘40g,内存4g,CPU4核心网络均采用NAT模式&#xff08;新建虚拟机默认的模式&#xff09; centos7镜像下载&#xff1a;https://mirrors.tuna.tsi…

Scala第十三章节

Scala第十三章节 1. 高阶函数介绍 2. 作为值的函数 3. 匿名函数 4. 柯里化 5. 闭包 6. 控制抽象 7. 案例: 计算器 scala总目录 文档资料下载

MacOS怎么配置JDK环境变量

1 输入命令看是否配置了JDk 的环境变量&#xff1a;echo $JAVA_HOME 要是什么也没输出 证明是没配置 2 输入命令编辑 sudo vim ~/.bash_profile 然后按 i &#xff0c;进入编辑模式&#xff0c;粘贴下面的代码&#xff0c;注意&#xff1a;JAVA_HOME后面路径需要改成自己的版…