C++多线程详细讲解-程序员宅基地

技术标签: C++  c++  多线程  

本文是纯转载,觉得大佬写的非常好!如有侵权可以删除
链接: link.

C++多线程基础教程
目录
1 什么是C++多线程?
2 C++多线程基础知识
2.1 创建线程
2.2 互斥量使用
lock()与unlock():
lock_guard():
unique_lock:
condition_variable:
2.3 异步线程
async与future:
shared_future
2.4 原子类型automic
实例
生产者消费者问题
4 C++多线程高级知识
4.1 线程池
线程池基础知识
线程池的实现
5 延伸拓展
最后一次更新日期:2020.08.23

1 什么是C++多线程?

线程:线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,进程包含一个或者多个线程。进程可以理解为完成一件事的完整解决方案,而线程可以理解为这个解决方案中的的一个步骤,可能这个解决方案就这只有一个步骤,也可能这个解决方案有多个步骤。
多线程:多线程是实现并发(并行)的手段,并发(并行)即多个线程同时执行,一般而言,多线程就是把执行一件事情的完整步骤拆分为多个子步骤,然后使得这多个步骤同时执行。
C++多线程:(简单情况下)C++多线程使用多个函数实现各自功能,然后将不同函数生成不同线程,并同时执行这些线程(不同线程可能存在一定程度的执行先后顺序,但总体上可以看做同时执行)。
上述概念很容易因表述不准确而造成误解,这里没有深究线程与进程,并发与并行的概念,以上仅为一种便于理解的表述,如果有任何问题还请指正,若有更好的表述,也欢迎留言分享。

2 C++多线程基础知识

2.1 创建线程

首先要引入头文件#include(C++11的标准库中提供了多线程库),该头文件中定义了thread类,创建一个线程即实例化一个该类的对象,实例化对象时候调用的构造函数需要传递一个参数,该参数就是函数名,thread th1(proc1);如果传递进去的函数本身需要传递参数,实例化对象时将这些参数按序写到函数名后面,thread th1(proc1,a,b);只要创建了线程对象(传递“函数名/可调用对象”作为参数的情况下),线程就开始执行(std::thread 有一个无参构造函数重载的版本,不会创建底层的线程)。
有两种线程阻塞方法join()与detach(),阻塞线程的目的是调节各线程的先后执行顺序,这里重点讲join()方法,不推荐使用detach(),detach()使用不当会发生引用对象失效的错误。当线程启动后,一定要在和线程相关联的thread对象销毁前,对线程运用join()或者detach()。
join(), 当前线程暂停, 等待指定的线程执行结束后, 当前线程再继续。th1.join(),即该语句所在的线程(该语句写在main()函数里面,即主线程内部)暂停,等待指定线程(指定线程为th1)执行结束后,主线程再继续执行。
整个过程就相当于你在做某件事情,中途你让老王帮你办一个任务(你办的时候他同时办)(创建线程1),又叫老李帮你办一件任务(创建线程2),现在你的这部分工作做完了,需要用到他们的结果,只需要等待老王和老李处理完(join(),阻塞主线程),等他们把任务做完(子线程运行结束),你又可以开始你手头的工作了(主线程不再阻塞)。

#include<iostream>
#include<thread>
using namespace std;
void proc(int a)
{
    
    cout << "我是子线程,传入参数为" << a << endl;
    cout << "子线程中显示子线程id为" << this_thread::get_id()<< endl;
}
int main()
{
    
    cout << "我是主线程" << endl;
    int a = 9;
    thread th2(proc,a);//第一个参数为函数名,第二个参数为该函数的第一个参数,如果该函数接收多个参数就依次写在后面。此时线程开始执行。
    cout << "主线程中显示子线程id为" << th2.get_id() << endl;
    th2.join()//此时主线程被阻塞直至子线程执行结束。
    return 0;
}

2.2 互斥量使用

什么是互斥量?

这样比喻,单位上有一台打印机(共享数据a),你要用打印机(线程1要操作数据a),同事老王也要用打印机(线程2也要操作数据a),但是打印机同一时间只能给一个人用,此时,规定不管是谁,在用打印机之前都要向领导申请许可证(lock),用完后再向领导归还许可证(unlock),许可证总共只有一个,没有许可证的人就等着在用打印机的同事用完后才能申请许可证(阻塞,线程1lock互斥量后其他线程就无法lock,只能等线程1unlock后,其他线程才能lock),那么,这个许可证就是互斥量。互斥量保证了使用打印机这一过程不被打断。

程序实例化mutex对象m,线程调用成员函数m.lock()会发生下面 3 种情况:
(1)如果该互斥量当前未上锁,则调用线程将该互斥量锁住,直到调用unlock()之前,该线程一直拥有该锁。
(2)如果该互斥量当前被锁住,则调用线程被阻塞,直至该互斥量被解锁。

互斥量怎么使用?

首先需要#include

lock()与unlock():

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;//实例化m对象,不要理解为定义变量
void proc1(int a)
{
    
    m.lock();
    cout << "proc1函数正在改写a" << endl;
    cout << "原始a为" << a << endl;
    cout << "现在a为" << a + 2 << endl;
    m.unlock();
}

void proc2(int a)
{
    
    m.lock();
    cout << "proc2函数正在改写a" << endl;
    cout << "原始a为" << a << endl;
    cout << "现在a为" << a + 1 << endl;
    m.unlock();
}
int main()
{
    
    int a = 0;
    thread proc1(proc1, a);
    thread proc2(proc2, a);
    proc1.join();
    proc2.join();
    return 0;
}

不推荐实直接去调用成员函数lock(),因为如果忘记unlock(),将导致锁无法释放,使用lock_guard或者unique_lock能避免忘记解锁这种问题。

lock_guard():
其原理是:声明一个局部的lock_guard对象,在其构造函数中进行加锁,在其析构函数中进行解锁。最终的结果就是:创建即加锁,作用域结束自动解锁。从而使用lock_guard()就可以替代lock()与unlock()。
通过设定作用域,使得lock_guard在合适的地方被析构(在互斥量锁定到互斥量解锁之间的代码叫做临界区(需要互斥访问共享资源的那段代码称为临界区),临界区范围应该尽可能的小,即lock互斥量后应该尽早unlock),通过使用{}来调整作用域范围,可使得互斥量m在合适的地方被解锁:

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;//实例化m对象,不要理解为定义变量
void proc1(int a)
{
    
    lock_guard<mutex> g1(m);//用此语句替换了m.lock();lock_guard传入一个参数时,该参数为互斥量,此时调用了lock_guard的构造函数,申请锁定m
    cout << "proc1函数正在改写a" << endl;
    cout << "原始a为" << a << endl;
    cout << "现在a为" << a + 2 << endl;
}//此时不需要写m.unlock(),g1出了作用域被释放,自动调用析构函数,于是m被解锁

void proc2(int a)
{
    
    {
    
        lock_guard<mutex> g2(m);
        cout << "proc2函数正在改写a" << endl;
        cout << "原始a为" << a << endl;
        cout << "现在a为" << a + 1 << endl;
    }//通过使用{}来调整作用域范围,可使得m在合适的地方被解锁
    cout << "作用域外的内容3" << endl;
    cout << "作用域外的内容4" << endl;
    cout << "作用域外的内容5" << endl;
}
int main()
{
    
    int a = 0;
    thread proc1(proc1, a);
    thread proc2(proc2, a);
    proc1.join();
    proc2.join();
    return 0;
}

lock_gurad也可以传入两个参数,第一个参数为adopt_lock标识时,表示不再构造函数中不再进行互斥量锁定,因此此时需要提前手动锁定。

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;//实例化m对象,不要理解为定义变量
void proc1(int a)
{
    
    m.lock();//手动锁定
    lock_guard<mutex> g1(m,adopt_lock);
    cout << "proc1函数正在改写a" << endl;
    cout << "原始a为" << a << endl;
    cout << "现在a为" << a + 2 << endl;
}//自动解锁

void proc2(int a)
{
    
    lock_guard<mutex> g2(m);//自动锁定
    cout << "proc2函数正在改写a" << endl;
    cout << "原始a为" << a << endl;
    cout << "现在a为" << a + 1 << endl;
}//自动解锁
int main()
{
    
    int a = 0;
    thread proc1(proc1, a);
    thread proc2(proc2, a);
    proc1.join();
    proc2.join();
    return 0;
}

unique_lock:
unique_lock类似于lock_guard,只是unique_lock用法更加丰富,同时支持lock_guard()的原有功能。
使用lock_guard后不能手动lock()与手动unlock();使用unique_lock后可以手动lock()与手动unlock();
unique_lock的第二个参数,除了可以是adopt_lock,还可以是try_to_lock与defer_lock;
try_to_lock: 尝试去锁定,得保证锁处于unlock的状态,然后尝试现在能不能获得锁;尝试用mutx的lock()去锁定这个mutex,但如果没有锁定成功,会立即返回,不会阻塞在那里
defer_lock: 始化了一个没有加锁的mutex;

lock_guard unique_lock
手动lock与手动unlock 不支持 支持
参数 支持adopt_lock 支持adopt_lock/try_to_lock/defer_lock

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;
void proc1(int a)
{
    
    unique_lock<mutex> g1(m, defer_lock);//始化了一个没有加锁的mutex
    cout << "不拉不拉不拉" << endl;
    g1.lock();//手动加锁,注意,不是m.lock();注意,不是m.lock();注意,不是m.lock()
    cout << "proc1函数正在改写a" << endl;
    cout << "原始a为" << a << endl;
    cout << "现在a为" << a + 2 << endl;
    g1.unlock();//临时解锁
    cout << "不拉不拉不拉"  << endl;
    g1.lock();
    cout << "不拉不拉不拉" << endl;
}//自动解锁

void proc2(int a)
{
    
    unique_lock<mutex> g2(m,try_to_lock);//尝试加锁,但如果没有锁定成功,会立即返回,不会阻塞在那里;
    cout << "proc2函数正在改写a" << endl;
    cout << "原始a为" << a << endl;
    cout << "现在a为" << a + 1 << endl;
}//自动解锁
int main()
{
    
    int a = 0;
    thread proc1(proc1, a);
    thread proc2(proc2, a);
    proc1.join();
    proc2.join();
    return 0;
}
unique_lock所有权的转移

mutex m;
{
      
    unique_lock<mutex> g2(m,defer_lock);
    unique_lock<mutex> g3(move(g2));//所有权转移,此时由g3来管理互斥量m
    g3.lock();
    g3.unlock();
    g3.lock();
}

condition_variable:
需要#include<condition_variable>;
wait(locker):在线程被阻塞时,该函数会自动调用 locker.unlock() 释放锁,使得其他被阻塞在锁竞争上的线程得以继续执行。另外,一旦当前线程获得通知(通常是另外某个线程调用 notify_* 唤醒了当前线程),wait() 函数此时再自动调用 locker.lock()。
notify_all():随机唤醒一个等待的线程
notify_once():唤醒所有等待的线程

2.3 异步线程

需要#include

async与future:
async是一个函数模板,用来启动一个异步任务,它返回一个future类模板对象,future对象起到了占位的作用,刚实例化的future是没有储存值的,但在调用future对象的get()成员函数时,主线程会被阻塞直到异步线程执行结束,并把返回结果传递给future,即通过FutureObject.get()获取函数返回值。

相当于你去办政府办业务(主线程),把资料交给了前台,前台安排了人员去给你办理(async创建子线程),前台给了你一个单据(future对象),说你的业务正在给你办(子线程正在运行),等段时间你再过来凭这个单据取结果。过了段时间,你去前台取结果,但是结果还没出来(子线程还没return),你就在前台等着(阻塞),直到你拿到结果(get())你才离开(不再阻塞)。

#include <iostream>
#include <thread>
#include <mutex>
#include<future>
#include<Windows.h>
using namespace std;
double t1(const double a, const double b)
{
    
	double c = a + b;
	Sleep(3000);//假设t1函数是个复杂的计算过程,需要消耗3秒
	return c;
}

int main() 
{
    
	double a = 2.3;
	double b = 6.7;
	future<double> fu = async(t1, a, b);//创建异步线程线程,并将线程的执行结果用fu占位;
	cout << "正在进行计算" << endl;
	cout << "计算结果马上就准备好,请您耐心等待" << endl;
	cout << "计算结果:" << fu.get() << endl;//阻塞主线程,直至异步线程return
        //cout << "计算结果:" << fu.get() << endl;//取消该语句注释后运行会报错,因为future对象的get()方法只能调用一次。
	return 0;
}

shared_future
future与shard_future的用途都是为了占位,但是两者有些许差别。
future的get()成员函数是转移数据所有权;shared_future的get()成员函数是复制数据。
因此:
future对象的get()只能调用一次;无法实现多个线程等待同一个异步线程,一旦其中一个线程获取了异步线程的返回值,其他线程就无法再次获取。
shared_future对象的get()可以调用多次;可以实现多个线程等待同一个异步线程,每个线程都可以获取异步线程的返回值。

future shared_future
语义 转移 赋值
可否多次调用 否 可

2.4 原子类型automic

原子操作指“不可分割的操作”;也就是说这种操作状态要么是完成的,要么是没完成的。互斥量的加锁一般是针对一个代码段,而原子操作针对的一般都是一个变量。
automic是一个模板类,使用该模板类实例化的对象,提供了一些保证原子性的成员函数来实现共享数据的常用操作。

可以这样理解:
在以前,定义了一个共享的变量(int i=0),多个线程会操作这个变量,那么每次操作这个变量时,都是用lock加锁,操作完毕使用unlock解锁,以保证线程之间不会冲突;
现在,实例化了一个类对象(automic I=0)来代替以前的那个变量,每次操作这个对象时,就不用lock与unlock,这个对象自身就具有原子性,以保证线程之间不会冲突。

automic对象提供了常见的原子操作(通过调用成员函数实现对数据的原子操作):
store是原子写操作,load是原子读操作。exchange是于两个数值进行交换的原子操作。
即使使用了automic,也要注意执行的操作是否支持原子性。一般atomic原子操作,针对++,–,+=,-=,&=,|=,^=是支持的。

实例
前一章内容为了简单的说明一些函数的用法,所列举的例子有些牵强,因此在本章列举了一些多线程常见的实例

生产者消费者问题
/*

生产者消费者问题
*/
#include <iostream>
#include <deque>
#include <thread>
#include <mutex>
#include <condition_variable>
#include<Windows.h>
using namespace std;

deque<int> q;
mutex mu;
condition_variable cond;
int c = 0;//缓冲区的产品个数

void producer() {
     
	int data1;
	while (1) {
    //通过外层循环,能保证生成用不停止
		if(c < 3) {
    //限流
			{
    
				data1 = rand();
				unique_lock<mutex> locker(mu);//锁
				q.push_front(data1);
				cout << "存了" << data1 << endl;
				cond.notify_one();  // 通知取
				++c;
			}
			Sleep(500);
		}
	}
}

void consumer() {
    
	int data2;//data用来覆盖存放取的数据
	while (1) {
    
		{
    
			unique_lock<mutex> locker(mu);
			while(q.empty())
				cond.wait(locker); //wati()阻塞前先会解锁,解锁后生产者才能获得锁来放产品到缓冲区;生产者notify后,将不再阻塞,且自动又获得了锁。
			data2 = q.back();//取的第一步
			q.pop_back();//取的第二步
			cout << "取了" << data2<<endl;
			--c;
		}
		Sleep(1500);
	}
}
int main() {
    
	thread t1(producer);
	thread t2(consumer);
	t1.join();
	t2.join();
	return 0;
}

4 C++多线程高级知识

4.1 线程池

线程池基础知识
不采用线程池时:

创建线程 -> 由该线程执行任务 -> 任务执行完毕后销毁线程。即使需要使用到大量线程,每个线程都要按照这个流程来创建、执行与销毁。

虽然创建与销毁线程消耗的时间 远小于 线程执行的时间,但是对于需要频繁创建大量线程的任务,创建与销毁线程 所占用的时间与CPU资源也会有很大占比。

为了减少创建与销毁线程所带来的时间消耗与资源消耗,因此采用线程池的策略:

程序启动后,预先创建一定数量的线程放入空闲队列中,这些线程都是处于阻塞状态,基本不消耗CPU,只占用较小的内存空间。

接收到任务后,线程池选择一个空闲线程来执行此任务。

任务执行完毕后,不销毁线程,线程继续保持在池中等待下一次的任务。

线程池所解决的问题:

(1) 需要频繁创建与销毁大量线程的情况下,减少了创建与销毁线程带来的时间开销和CPU资源占用。(省时省力)

(2) 实时性要求较高的情况下,由于大量线程预先就创建好了,接到任务就能马上从线程池中调用线程来处理任务,略过了创建线程这一步骤,提高了实时性。(实时)

线程池的实现
待更新。

延伸拓展

创建类,除了传递函数外,还可以使用:Lambda表达式、可调用类的实例。
线程与进程:
并发与并行:
并发与并行并不是非此即彼的概念
并发:同一时间发生两件及以上的事情。
线程并不是越多越好,每个线程都需要一个独立的堆栈空间,线程切换也会耗费时间。
并行:

detach():

未完待续

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_45439571/article/details/115015331

智能推荐

Vue项目搭建常用的配置文件,request.js和vue.config.js_interceptors.request.use 没有config-程序员宅基地

文章浏览阅读6.4w次,点赞248次,收藏1k次。笔记_interceptors.request.use 没有config

OpenShift 4.5 新特性 - 创建任务和定时任务_openshift cronjob-程序员宅基地

文章浏览阅读1.1k次。文章目录通过YAML创建创建Job创建CronJob使用命令创建Job创建CronJob在Kubernetes中分贝使用Job和CronJob实现一次性运行的任务和定时运行的的任务,他们分别被Kubernetes的JobController和CronJobController控制器所控制,而这些任务都是通过Pod运行的。在创建Job和CronJob对象的时候,既可以使用定义对象的YAML文件,还可使用命令直接创建。需要注意的是,从OpenShift 4.5开始,在使用oc命令创建Job和CronJob对_openshift cronjob

前端js实现canves画布中拖拽、放大、缩小、旋转图片和文字,设置背景图片,导出_fabric.js 截取固定大小图片-程序员宅基地

文章浏览阅读2.7k次。最近在研究canves,想实现一个可以在画布中操作上传的内容,不经意间发现了个插件Fabric.js。Fabric.js 是一个强大的H5 canvas框架,在原生canvas之上提供了交互式对象模型,通过简洁的api就可以在画布上进行丰富的操作。image。_fabric.js 截取固定大小图片

iOS 音频的录制、播放及音频文件管理_updatemeters-程序员宅基地

文章浏览阅读2.1w次,点赞2次,收藏6次。音频会话音效播放音乐播放音频录制音频管理音频队列服务参考地址_updatemeters

深入理解机械臂动力学建模_机械臂 组合体惯量法-程序员宅基地

文章浏览阅读1.8w次,点赞4次,收藏87次。A刚性机械臂机械臂建模是机械臂控制的基础,控制效果的好坏很大程度上决定于所建立的动力学模型的准确性。目前对刚性机械臂的动力学建模方法较多,理论较为成熟。而对于柔性空间机械臂的精确建模尚处在研究阶段。 表格1 刚体动力学建模原理..._机械臂 组合体惯量法

npm install 报错:gyp ERR! configure error gyp ERR! stack Error: EACCES: permission denied, mkdir '/Us_npm configure error报错-程序员宅基地

文章浏览阅读4k次。没有权限解决方案:sudo chown -R $USER /Users/huzhiqi/Downloads/web/projects/tag_web/node_modules/node-sass/注意:要看清是哪里没有权限,再给没有权限的文件夹设置权限。..._npm configure error报错

随便推点

GitLab配置ssh-key_gitlab更新ssh-key-程序员宅基地

文章浏览阅读1.9k次。1 背景当前很多公司都选择git作为代码版本控制工具,然后自己公司搭建私有的gitlab来管理代码,我们在clone代码的时候可以选择http协议,当然我们亦可以选择ssh协议来拉取代码。但是网上很少找到如何用git客户端生成ssh key,然后配置在gitlab,我当时在做的时候苦于摸索,后来终于找到了解决方案,那么本文,我们就来聊一聊如何本地git客户端生成ssh key,然后配置在gitlab里,而后使用ssh协议进行提交和拉取git远程仓库的代码。2 解决方案打开本地git bash,使用_gitlab更新ssh-key

计算机网络考试试题库-期末考试题库含答案_某公司 testa 有一个总部和三个下属工厂。总部有 4 个局域网,其 中 lan2-lan4 都-程序员宅基地

文章浏览阅读1.2w次,点赞33次,收藏332次。一、选择题(第一章 1-10;第二章 11-20;第三章21-35;第四章36-60 ;第五章 61-73道;第六章 74-84道;第七章85-90;第九章91-95;第十章96-100)1.下列四项内容中,不属于Internet(因特网)基本功能是____D____。A.电子邮件 B.文件传输 C.远程登录 D.实时监测控制2.Internet是建立在____C_____协议集上的国际互联网络。 A.IPX B.NetBEUI C.TCP/IP _某公司 testa 有一个总部和三个下属工厂。总部有 4 个局域网,其 中 lan2-lan4 都

高通平台GPU动态调频DCVS . 篇1 . Interface_高通 gpu 限频 /sys/class/kgsl/kgsl-3d0/max_pwrlevel-程序员宅基地

文章浏览阅读9.4k次,点赞13次,收藏43次。高通平台的GPU内核驱动架构趋于稳定,代码和接口都具备通用性,故分析整理出来以供快速参考高通平台GPU内核驱动框架全称是 Kernel-Graphics-Support-Layer KGSL1. KGSL kernel interfacekgsl驱动所暴露出来的GPU相关常规控制接口位于 /sys/class/kgsl/kgsl-3d0 路径下/sys/class/kgsl/kgsl-3d..._高通 gpu 限频 /sys/class/kgsl/kgsl-3d0/max_pwrlevel

网络安全系列-XI: 主流网络协议介绍_xiip-程序员宅基地

文章浏览阅读4.2k次。本文针对主流的网络协议进行介绍_xiip

正则表达式-实数_实数正则判断-程序员宅基地

文章浏览阅读3k次。整数整数包括:0,正整数,负整数00的正则:^0$正整数正整数(必须为1-9开头,后面[0-9]0个或多个)的正则:^[1-9]\d*$负整数负整数(正整数前加"-"):^\-[1-9]\d*$0,正整数和负整数合并起来就是整数:^-?[1-9]\d*|0$小数(这里说的时末尾可以为0的小数)小数就是整数加上小数点再加上1个或多个[0-9],即^(\-?[1-9]\d*|0)\.\d+$"|"会作用于全部范围,所以要加括号。(这里说的时末尾不为0的小数)小数就是整_实数正则判断

RabbitMQ高级特性(消息可靠性投递 ACK TTL+死信队列 延迟队列 日志与监控 消息可靠性分析与追踪 消息可靠性保障 消息幂等性处理)_消息可靠性等级-程序员宅基地

文章浏览阅读500次。RabbitMQ高级特性RabbitMQ一、RabbitMQ高级特性1.1 消息可靠性投递搭建consumerproviderconfirmCallback 确认模式return 退回模式1.2Consumer ACK1.3 消费端限流1.4 TTL1.5 死信队列1.6 延迟队列1.7 日志与监控1.8 消息可靠性分析与追踪二、RabbitMQ应用问题2.1 消息可靠性保障2.2 消息幂等性处理RabbitMQ一、RabbitMQ高级特性1.1 消息可靠性投递在使用 RabbitMQ 的时候,作为_消息可靠性等级