C++高并发多线程学习
1.简介
C++11是2011年发布的,相信 Linux 程序员都用过 Pthread, 但有了 C++11 的 std::thread 以后,你可以在语言层面编写多线程程序了,直接的好处就是多线程程序的可移植性得到了很大的提高,所以作为一名 C++ 程序员,熟悉 C++11 的多线程编程方式还是很有益处的。
多线程是多任务处理的一种特殊形式,多任务处理允许让电脑同时运行两个或两个以上的程序。
多线程程序包含可以同时运行的两个或多个部分。这样的程序中的每个部分称为一个线程,每个线程定义了一个单独的执行路径。
一般情况下,两种类型的多任务处理:基于进程和基于线程。
基于进程的多任务处理是程序的并发执行。
基于线程的多任务处理是同一程序的片段的并发执行。
并发和并行:
说到多线程编程,那么就不得不提并行和并发,多线程是实现并发(并行)的一种手段。并行是指两个或多个独立的操作同时进行。注意这里是同时进行,区别于并发,在一个时间段内执行多个操作。在单核时代,多个线程是并发的,在一个时间段内轮流执行;在多核时代,多个线程可以实现真正的并行,在多核上真正独立的并行执行。例如现在常见的4核4线程可以并行4个线程;4核8线程则使用了超线程技术,把一个物理核模拟为2个逻辑核心,可以并行8个线程。所以,通常,要实现并发有两种方法:多进程和多线程。
多线程并发比多进程并发优势:
多线程并发指的是在同一个进程中执行多个线程。有操作系统相关知识的应该知道,线程是轻量级的进程,每个线程可以独立的运行不同的指令序列,但是线程不独立的拥有资源,依赖于创建它的进程而存在。也就是说,同一进程中的多个线程共享相同的地址空间,可以访问进程中的大部分数据,指针和引用可以在线程间进行传递。这样,同一进程内的多个线程能够很方便的进行数据共享以及通信,也就比进程更适用于并发操作。由于缺少操作系统提供的保护机制,在多线程共享数据及通信时,就需要程序员做更多的工作以保证对共享数据段的操作是以预想的操作顺序进行的,并且要极力的避免死锁(deadlock)。
2.与 C++11 多线程相关的头文件:
C++11 新标准中引入了四个头文件来支持多线程编程,他们分别是<atomic> ,<thread>,<mutex>,<condition_variable>和<future>。
1 thread:该头文件主要声明了 std::thread 类,另外 std::this_thread 命名空间也在该头文件中。
2 mutex:该头文件主要声明了与互斥量(mutex)相关的类,包括 std::mutex 系列类,std::lock_guard, std::unique_lock, 以及其他的类型和函数。
3 atomic:该头文主要声明了两个类, std::atomic 和 std::atomic_flag,另外还声明了一套 C 风格的原子类型和与 C 兼容的原子操作的函数。
4 condition_variable:该头文件主要声明了与条件变量相关的类,包括 std::condition_variable 和 std::condition_variable_any。
5 future:该头文件主要声明了 std::promise, std::package_task 两个 Provider 类,以及 std::future 和 std::shared_future 两个 Future 类,另外还有一些与之相关的类型和函数,std::async() 函数就声明在此头文件中。
3.各个主要功能模块概述
3.1线程std::thread
1 创建std::thread,一般会绑定一个底层的线程。若该thread还绑定好函数对象,则即刻将该函数运行于thread的底层线程。
2 线程相关的很多默认是move语义,因为在常识中线程复制是很奇怪的行为。
3 joinable():是否可以阻塞至该thread绑定的底层线程运行完毕(倘若该thread没有绑定底层线程等情况,则不可以join)
4 join():本线程阻塞直至该thread的底层线程运行完毕。
5 detach():该thread绑定的底层线程分离出来,任该底层线程继续运行(thread失去对该底层线程的控制)。
3.2互斥变量std::mutex,std::lock_guard,std::unique_lock
为了避免多线程对共享变量的一段操作会发生冲突,引入了互斥体和锁。
1 std::mutex
1.1 互斥体,一般搭配锁使用,也可自己锁住自己(lock(),unlock())。
1.2 若互斥体被第二个锁请求锁住,则第二个锁所在线程被阻塞直至第一个锁解锁。
2 std::lock_guard
简单锁,构造时请求上锁,释放时解锁,性能耗费较低。适用区域的多线程互斥操作。
3 std::unique_lock
更多功能也更灵活的锁,随时可解锁或重新锁上(减少锁的粒度),性能耗费比前者高一点点。适用灵活的区域的多线程互斥操作。
3.3原子变量std::atomic
原子变量的意思就是单个最小的、不可分割的变量(例如一个int),原子操作则指单个极小的操作(例如一个自增操作)
C++的原子类封装了这种数据对象,使多线程对原子变量的访问不会造成竞争。(可以利用原子类可实现无锁设计)
1 std::atomic_flag
1.1 它是一个原子的布尔类型,可支持两种原子操作。(实际上mutex可用atomic_flag实现)
1.2 test_and_set(): 如果atomic_flag对象被设置,则返回true; 如果atomic_flag对象未被设置,则设置之,返回false。
1.3clear():清除atomic_flag对象。
2 std::atomic
2.1对int, char, bool等基本数据类型进行原子性封装(其实是特化模板)。
2.2 store():修改被封装的值。
2.3 load(): 读取被封装的值。
————————————————
3.4 条件变量condition_variable
条件变量一般是用来实现多个线程的等待队列,即主线程通知(notify)有活干了,则等待队列中的其它线程就会被唤醒,开始干活。
1 std::condition_variable
2 wait(std::unique_lock < std::mutex > & lock, Predicate pred = [] (){return true;}):pred()为true时直接返回,pred()为false时,lock必须满足已被当前线程锁定的前提。执行原子地释放锁定,阻塞当前线程,并将其添加到等待*this的线程列表中。
3notify_one()/notify_all():激活某个或者所有等待的线程,被激活的线程重新获得锁。
虚假唤醒:
处于等待的添加变量可以通过notify_one/notify_all进行唤醒,调用函数进行信号的唤醒时,处于等待的条件变量会重新进行互斥锁的竞争。
没有得到互斥锁的线程就会发生等待转移(wait morphing),从等待信号量的队列中转移到等待互斥锁的队列中,一旦获取到互斥锁的所有权就会接着向下执行,
但是此时其他线程已经执行并重置了执行条件(例如一个活只需要两个线程来干,通知完两个线程后重置执行条件),这可能导致该线程执行引发未定义的错误。
3.5获取方 std::future
1 std::future
1.1 用于访问共享状态(即获取值)。
1.2 当future的状态还不是ready时就调用一个绑定的promise, packaged_task等的析构函数,会在期望里存储一个异常。
1.4 share():分享同一个共享状态给另一个future
1.3 wait():若共享状态不是ready,则阻塞直至ready。
1.4 get():获得共享状态的值,若共享状态不是ready,则阻塞直至ready。
1.5 std::future有局限性,在很多线程等待时,只有一个线程能获取等待结果。
2 std::shared_future
2.1 当需要多个线程等待相同的事件的结果(即多处访问同一个共享状态),需要用std::shared_future来替代std::future。
2.2 shared_future与future类似,但shared_future可以拷贝、多个shared_future可以共享某个共享状态的最终结果(即共享状态的某个值或者异常)。
2.3 shared_future可通过某个future对象隐式转换,或通过future::share()显示转换,无论哪种转换,被转换的那个future对象都会变为not-valid
3.6 提供方std::promise
1 std::promise
1.1 构造时,产生一个未就绪的共享状态(包含存储的T值和是否就绪的状态)。可设置T值,并让状态变为ready。
1.2 get_future():共享状态绑定到future对象。
1.3 set_value():设置共享状态的T值,并让状态变为ready,则绑定的future对象可get()。
2 std::packaged_task
2.1 构造时绑定一个函数对象,也产生一个未就绪的共享状态。通过thread启动或者仿函数形式启动该函数对象。
2.2 但是相比promise,没有提供set_value()公用接口,而是当执行完绑定的函数对象,其执行结果返回值或所抛异常被存储于能通过 std::future 对象访问的共享状态中。
2.3 get_future():共享状态绑定到future对象。
3.7 异步操作 std::async
1 std::async(std::launch::async | std::launch::deferred, Func, Args…)
1.1 异步执行一个函数,其函数执行完后的返还值绑定给使用std::async的futrue(其实是封装了thread,packged_task的功能,使异步执行一个任务更为方便)。
1.2若用创建std::thread执行异步行为,硬件底层线程可能不足,产生错误。而std::async将这些底层细节掩盖住,如果使用默认参数则与标准库的线程管理组件一起承担线程创建和销毁、避免过载、负责均衡的责任。
1.3 所以尽量使用以任务为驱动的async操作设计,而不是以线程为驱动的thread设计。
std::async中的第一个参数是启动策略,它控制std::async的异步行为,我们可以用三种不同的启动策略来创建std::async:
2 std::launch::async参数 保证异步行为,即传递函数将在单独的线程中执行。
3 std::launch::deferred参数 当其他线程调用get()/wait()来访问共享状态时,将调用非异步行为。
4 std::launch::async | std::launch::deferred参数 是默认行为。有了这个启动策略,它可以异步运行或不运行,这取决于系统的负载。
————————————————
版权声明:本文为CSDN博主「su扬帆启航」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/orange_littlegirl/article/details/102718884