1. 线程
在一个应用程序(进程)中同时执行多个小的部分(线程),这就是多线程。多个线程虽然共享一样的数据,但是却执行不同的任务。
线程启动、创建、结束
主线程执行完了,就代表整个进程执行完毕了,此时,一般情况下如果其他子线程没有执行完毕,那么这些子线程也会被操作系统终止。所以为了让子线程执行完就需要让主线程保持住。但是也有意外,不一定需要保持住。
detach():传统多线程,主线程要等待子线程执行完毕,然后自己再最后退出, detach分离,也就是主线程和子线程不汇合了,各自独立执行,主线程执行完了,不影响我子线程。为什么引入detach?如果创建了很多子线程,让主线程逐个等待子线程结束,这个编程方法不好。
一旦detach()之后,与主线程的thread对象就会与主线程失去关联,此时子线程就驻留在后台。这个子线程就有运行时库负责清理相关线程的资源(守护线程)。一旦detach()调用后,就不能再用join()了。
joinable()判断是否可以成功使用join()或者detach()。
示例:用类对象创建线程
class Ta{
public:
void operator()() //需要有这个函数,且不能带参数
{
//你需要执行的操作
}
};
int main(){
Ta ta;
thread thread(ta);//这样线程就会执行类的operator()
//ta.join()
ta.detach()//如果 ta中有引用或指针主线程的数据,就会出现不可意料的值。因为一旦主线程执行完,相应的资源就被释放了。
//但是对象本身ta还在吗?不在了。那为什么thread还能正常运行?因为创建thread时创建的副本在子线程中运行。不行你可以显示实现一个拷贝构造函数看看。
}
如果会对线程进行detach()。那么在创建线程时传参要特别注意:
所以建议不要轻易使用detach(),尽量使用join()。这样就不存在局部变量失效导致线程对内存的非法引用问题。
2. 并发的概念
两个或者多个任务(独立的活动)同时的进行:一个程序执行多个独立任务。
以往计算机,单核cpu:某一时刻只能执行一个任务,由任务系统调度,每秒钟进行多次所谓的任务切换。这是一种并发的假象,不是真正的并发,这种切换(上下文切换)是有时间开销的。比如操作系统要保存你操作状态,执行进度等信息,都需要时间,一会切换回来,恢复这些信息也需要时间。
多核cpu才是真正的并发(硬件并发)
使用并发的原因,主要是同时可以干多个事,提高效率。
多线程并发
C++11可以通过多线程实现并发,这是一种比较底层、传统的实现方式。C++11引入了5个头文件来支持多线程编程:<atomic>/<thread>/<mutex>/<condition_variable>/<future>
线程并不是越多越好,每个线程,都需要一个独立的堆栈空间(1M),线程之间的切换要保存很多中间状态;
创建的线程数量不建议超过200-300个,在实际项目中可以不断调整和优化。
多线程通讯共享内存,全局变量,指针,引用等都可以实现。
共享内存带来问题:数据一致性问题,可以用信号量技术来解决。
多进程并发比多进程好, 启动速度快,更轻量级 系统资源开销更好,速度快,共享内存这种通讯方式比任何其他方式都快。缺点有一定难度,要小心处理数据的一致性问题。
主线程等待所有子线程运行结束,最后主线程结束。这样更容易写出稳定的程序。
vector<thread> threads; //用容器管理多线程
线程函数内,调用 std::this_thrad::get_id()
可以获取线程id。
临界区,互斥量等以往多线程代码不能跨平台, poxi 可以跨平台,但是开发麻烦。
并发实现的常用框架
3. std::mutex 互斥访问
<mutex>是C++标准程序库中的一个头文件,定义了C++11标准中一些互斥访问的类与方法。
其中std::mutex表示普通互斥锁,可以与std::unique_lock配合使用,把std::mutex放到unique_lock中时,mutex会自动上锁,unique_lock析构时,同时把mutex解锁。因此std::mutex可以保护同时被多个线程访问的共享数据,并且它独占对象所有权,不支持对对象递归上锁。
可以这样理解:各个线程在对共享资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。(下图来自网络)
常用的成员函数有:
死锁问题
死锁问题,是至少由两个锁头也就是两个互斥量才能产生。线程A锁了金锁,需要去锁住银锁;然而线程B锁了银锁,需要去锁住金锁,这样A和B都在等待对方释放锁。就成了死锁。
死锁的一般解决方案:只要保证两个互斥量上锁的顺序一致就不会死锁。std::lock()函数模板 能力:一次锁住两个或者两个以上的互斥量。它不存这种因为在多个线层中,因为锁的顺序问题导致死锁的风险问题
std::lock() 如果互斥量中有一个没锁住,它就在那里等着。其情况就是多个锁要么都锁住,要么都没锁住。如果只锁了一个,另外一个没锁住,它立即把已经锁上的释放掉。
但是需要调用mutex.unlock()区分别释放锁。为了避免遗忘unlock,这里可以借用lock_guard和adopt_lock去解决这个问题。
std::lock(my_mutex1,my_mutex2);
std::lock_guard<std::mutex> mylock(my_mutex1,std::adopt_lock);
std::lock_guard<std::mutex> mylock(my_mutex2,std::adopt_lock);
std::adopt_lock是一个结构体对象,起一个标记作用,作用就是表示这个互斥量已经lock了,在lock_guard()构造函数中不需要进行lock了,这样就不用去手动释放unlock了,借用lock_guard()的析构函数去unloc。
4. std::unique_lock 锁管理模板类
std::unique_lock为锁管理模板类,是对通用mutex的封装。
std::unique_lock对象以独占所有权的方式(unique owership)管理mutex对象的上锁和解锁操作,即在unique_lock对象的声明周期内,它所管理的锁对象会一直保持上锁状态;而unique_lock的生命周期结束之后,它所管理的锁对象会被解锁。因此用unique_lock管理互斥对象,可以作为函数的返回值,也可以放到STL的容器中。
其常用的成员函数为:
unique_lock是个类模板,工作中,一般推荐使用lock_guard;
unique_lock比lock_guard灵活的多,但是效率要比lock_guard差一点,内存占用多一点。
unique_lock第二个参数:
表示互斥量已经被lock了(你必须要把互斥量lock,否则会报异常 std::adopt_lock标记的效果就是“假设调用方线程已经拥有了互斥的所有权(已经lock成功了),lock_guard和unique_lock都可以调用这个标记。
表示我们会尝试用mutex的lock去锁定这个mutex,如果没有锁定成功,就立即返回,并不会阻塞在哪里。用try_to_lock的前提是你自己不能先去lock.
std::mutex mymutex;
std::unique_lock<std::mutex> example(my_mutex,std::try_to_lock);
if(example.owns_lock()){
//拿到锁干什么。。。
}else{
// 没拿到锁干什么
}
前提是不能自己先lock,否则会报异常 defer_lock意思就是并没有给mutex加锁,初始化了一个没有加锁mutex
std::unique_lock<mutex> example(my_mutex,std::defer_lock);//没有加锁
/*用法一
example.lock();//自己手动加锁
//一些共享代码要处理
example.unlock();
//处理一些非共享代码
example.lock();//不需要手动开锁,当然你要手动开锁也可以,只是画蛇添足
*/
//用法二
if(example.try_lock()==true){
//拿到锁,做事
}else{
//没拿到锁做事
}
unique_lock成员函数
lock() //可以手动加锁,手动加锁后,你可以不用手动开锁,离开作用域后自动放锁。
unlock() try_lock() 尝试给互斥量加锁,不阻塞。拿到就true,没拿到就false release(),返回它所管理的mutex对象指针,并释放所有权,也就是说这个unique_lock和mutext不再有关系。如果原来的mutex处于加锁状态,你接管过来必须负责开锁!!
无锁编程
为什么有时候需要unlock?因为你lock锁住的代码越少,执行越快,整个程序运行效率越高。有人也把锁头锁住的代码多少称为锁的粒度。粒度一般用粗细来描述. 锁住的代码少,这个粒度叫细,执行效率高. 锁住的代码多,这个粒度就粗,执行效率低. 合适的粒度,粒度太小可能遗漏保护代码.选择合适的粒度,是高级程序员的能力和实力的体现
std::unique_lock<std::mutex> example(my_mutex);
//example可以把自己的mutex所有权转移给其他unique_lock对象,但是不能复制所有权!
std::unique_lock<std::mutex> example(std::move(example));
return std::unique_lock<std::mutex> //通过函数返回值也是一种所有权转移
5. std::condition_variable 条件变量
<condition_variable>是C++标准程序库中的一个头文件,定义了C++11标准中的一些用于并发编程时表示条件变量的类与方法等。
条件变量的引入是为了作为并发程序设计中的一种控制结构。当多个线程访问同一共享资源时,不但需要用互斥锁实现独享访问以避免并发错误(竞争危害),在获得互斥锁进入临界区后还需要检验特定条件是否成立:
条件变量std::condition_variable用于多线程之间的通信,它可以阻塞一个或同时阻塞多个线程。std::condition_variable需要与std::unique_lock配合使用。
常用成员函数:
(1)构造函数:仅支持默认构造函数。
(2)wait():当前线程调用wait()后将被阻塞,直到另外某个线程调用notify_*唤醒当前线程。当线程被阻塞时,该函数会自动调用std::mutex的unlock()释放锁,使得其它被阻塞在锁竞争上的线程得以继续执行。一旦当前线程获得通知(notify,通常是另外某个线程调用notify_*唤醒当前线程),wait()函数自动调用std::mutex的lock()。wait分为无条件被阻塞和带条件的被阻塞两种:
(3)notify_all: 唤醒所有的wait线程,如果当前没有等待线程,则该函数什么也不做。
(4)notify_one:唤醒某个wait线程,如果当前没有等待线程,则该函数什么也不做;如果同时存在多个等待线程,则唤醒某个线程是不确定的(unspecified)。
简单的说就是,当std::condition_variable对象的某个wait函数被调用的时候,它使用std::unique_lock(通过std::mutex)来锁住当前线程。当前线程会一直被阻塞,直到另外一个线程在相同的std::condition_variable对象上调用了notification函数来唤醒当前线程。
6. async、future、packaged_task、promise异步编程
创建后台任务并返回值 。希望线程返回一个结果 std::async是个函数模板,用来启动一个异步任务,它返回一个std::future对象,std::future是一个类模板.。
异步任务:自动创建一个线程并开始执行对应的线程入口函数,他返回一个std::future对象 这个future对象里面含有线程入口函数所返回的结果,我们可以通过调用future对象的成员函数get()来获取结果.
通过额外向std::async()传递一个参数,该参数类型是std::launch类型(枚举类型),来达到一些目的
std::launch::deferred
:表示线程入口函数调用被延迟到std::future的wait()或者get()函数调用时才执行. 如果wait()或者get()没有调用则不会执行线程.
eg: std::async(std::launch::deferred,my_thread)
可以测试线程id,延迟调用,其实没有创建新线程,是在主线程中调用的线程入口函数.std::launch::async
在调用async函数时,就开始创建线程 async函数默认用的就是std::launch::async
标记#include <future>
#include <thread>
#include<iostream>
int my_thread(){
return std::this_thread::get_id();
}
int main(){
//auto res=std::async(my_thread);
std::future<int> res=std::async(my_thread);//创建了线程并执行,也可以成员函数创建.
//A a;
//std::future<int> res=std::async(&A::memberfunc,&a,&func_para);
//cout<<res.get()<<endl; //卡在这里等待mythrea()执行完毕,拿到结果
// res.get()只能调用一次!!
res.wait();//等待线程返回,但本身并不返回结果.
}
packaged_task包装起来的可调用对象还可以直接调用。
int mythread1(int i){}
int my_thread2(){}
int main(){
std::packaged_task<int(int)>> mypt(mythread1) //把函数mythread1通过
//packaged_task包装起来
std::thread t1(std::ref(mypt),1)//线程直接开始执行,第二个参数作为线程入口函数的参数
t1.join();
std::future<int> res=mypt.get_future();
//std::future对象里包含线程入口函数的返回结果
cout<<res.get();
}
我们能够在某个线程中给他赋值,然后我们可以在其他线程中,把这个值取出来. 总结:通过promise保存一个值,在将来某个时刻我们通过把一个future绑定到promise上,来获取这个绑定值.
void mythread(std::promise<int>& tmpp,int calc){
int i=1/calc;
int res=i+calc;
tmpp.set_value(res)
return ;
}
int main(){
std::promise<int> myprom;
std::thread t1(mythread,std::ref(&myprom),180);
t1.join();
//获取结果值
std::future<int> fu=myprom.get_future();//promise和future绑定,用于获取线程返回值
auto result=fu.get();
}
std::future<int> res=std::async(my_thread);
std::future_status status=res.wait_for(std::chrono::seconds(1)); //等待1秒
if(status==std::future_status::timeout){
//超时:表示等待了1秒线程没执行完
//do something
}else if(status==std::future_status::ready) {
//表示线程成功返回
cout<<res.get()<<endl;
}else if(status==std::future_status::deferred){
//如果async的第一个参数为std::launch::deferred,则本条件成立
//线程被延迟执行
res.get();
}
std::future对象只能get一次,因为get函数的设计是一个移动语义.再次对对象进行get时,自然就没东西可以get了.
std::shared_future是个类模板,它的get()函数是复制数据,可以实现多次get
std::future<int> res=my.get_future();
//std::shared_future<int> res_s(std::move(res));
//或者
std::shared_furture<int> res_S(res.share());//此时res_s有值,res里空了
7. std::atomic 原子操作
<atomic>是C++标准程序库中的一个头文件,定义了C++11标准中一些表示线程、并发控制时进行原子操作的类与方法,主要声明了两大类原子对象:std::atomic和std::atomic_flag。
CAS是Compare And Swap的简称。意味比较并交换。
首先要说的是,为了保证Volatile的原子性操作,引入了三种方法。并且原子操作性能最高。他就是CAS。
那为什么CAS性能最好呢??
我们要来分析一下synchronized低效的原因:首先呢synchronized同步过程大家都有了解。不过这些过程不论是刷新主内存还是本地内存,都需要占用很多资源,所以性能低下。
再来了解CAS的原理:它有三个参数,当前内存值V,旧的预期值A,更新值B,只有当内存值和预期值相同时候,才会修改为B,否则就通过自旋锁的方式再次尝试,直到成功。(显然自旋次数过多也会造成影响)。然而CAS的过程其实没有获取和释放锁。它的运行和JMM内存模型没有关系。而是通过native方法调用本地方法直接和硬件打交道(使用了Unsafe unsafe = Unsafe.getUnsafe()。Unsafe 是CAS的核心类,提供的是硬件级别的原子操作)。因此性能更快。
怎么解决ABA问题呢?于是呢就想出了版本控制这一个方法。我们在每一个变量上都加入一个版本号。改变的时候版本号增加,比较的时候版本号一同比较。
原子操作的主要特点是原子对象的并发访问不存在数据竞争,利用原子对象可实现数据结构的无锁设计。在多线程并发执行时,原子操作是线程不会被打断的执行片段。
(1)atomic_flag类
(2)std::atomic类
std::atomic 常用的成员函数:
参考文献:
1. blog.csdn.net/liuxuejia
2. blog.csdn.net/fengbingc
3. cnblogs.com/wangshaowei
4. blog.csdn.net/fengbingc
5. cnblogs.com/taiyang-li/
6. blog.csdn.net/tanningzh
7.https://zhuanlan.zhihu.com/p/134099301
8.https://zhuanlan.zhihu.com/p/136861784
本文仅作学术交流,如有侵权,请联系删文