并发与多线程
一、并发基本概念及其实现,进程、线程基本概念。
1.并发、进程、线程的基本概念和综述
(1.1)并发:两个或者更多的任务(独立的活动)同时进行;一个程序同时执行多个任务;以往计算机,单核CPU,某一时刻只能执行一个任务,由操作系统调度,每秒钟进行多次所谓的“任务切换”,并发的假象,不是真正的并发,这种切换叫上下文切换,是有时间开销的,比如操作系统要保存切换时的各种状态、执行的进度等信息,都需要时间,一会儿切换回来的时候需要复原这些信息。
多核处理器,能实现真正的并行执行多个任务(硬件并发);
使用并发的原因:主要是同时干多个事情,提高性能。
(1.2)可执行程序:磁盘上的一个文件,Windows下,一个扩展名为.exe的,Linux下 ,ls -la, rwxrwxrwx(x执行权限)。
(1.3)进程:Windows下双击一个可执行程序,Linux下,./文件名。进程就是一个可执行程序运行起来,就创建了一个进程。
(1.4)线程:1)每个进程就是执行起来的可执行程序,都有一个主线程,这个主线程是唯一的,一个进程中只能有一个主线程。
2)当你执行可执行程序,产生了一个进程后,这个主线程就随着这个进程默默的启动起来了。程序运行起来的时候,实际上是进程中的主线程执行这个main函数的代码;主线程和进程不可分割。
线程:用来执行代码的,理解成一条代码的执行道路。除了主线程,可以通过代码创建其他线程。
每创建一个新线程,就可以在同一时刻,可以多做一件事。
多线程并发,线程不是越多越好,每个线程都需要一个独立的堆栈空间(1M),线程之间切换要保存很多中间状态;切换回耗费本该程序运行的时间。
线程总结:1)线程用来执行代码的;b)把线程这个东西理解成执行通路,一个新线程代表一个新通路。c)一个进程自动包含一个主线程,主线程随着进程默默的启动并运行,可以通过编码来创建其他的线程。d)因为主线程是自动启动的,一个进程中最少有一个主线程。e)多线程程序就是可以同时做多个事。
主线程从main()函数开始执行。
2.并发的实现方法:多进程并发,多线程并发。
(2.1)多进程并发:进程之间通信:同一个电脑,管道,文件,消息队列,共享内存等;不同电脑,socket通信技术。
(2.2)多线程并发:一个进程中的所有线程共享地址空间(共享内存),全局变量,指针,引用都可以在线程之间传递:使用多线程开销小于多进程。
共享内存的问题:数据一致性问题。
(2.3)总结:和进程相比,线程有如下优点:a.线程启动速度更快,更轻量级;b.系统开销更小,执行速度跟块,比如共享内存这种通信方式比任何其他的通信方式更快。
3.C++11新标准线程库
二、线程启动、结束,创建线程方法、join和detach
1.线程运行的开始和结束
主线程从main()开始执行,自己创建的线程,也需要从一个函数开始运行(初始函数),一旦这个函数运行完毕,就代表这这个线程结束。
整个进程时候执行完毕的标志是:主线程是否执行完毕,如果逐项城执行完毕了,就代表着整个进程执行完毕了;
一般情况下,如果其他子线程没有执行完毕,但是主线程执行完毕了,这些子线程也会被操作系统强行终止。
一般情况下, 如果要保持子线程的运行状态的话,那么就要让主线程一直运行。(也有例外使用detach())
a)包含头文件;b)初试函数
(1.1)thread:是标准库里的类。如thread myobj(myprint);创建了线程,线程执行入口为myprint(),myprint()线程开始执行。
不加join()或者detach()时,可能会报异常。
(1.2)join():加入、汇合,阻塞主线程,让主线程等待子线程执行完毕,然后子线程和主线程汇合,然后主线程继续执行。 如:myobj.join();当子线程执行完毕,继续执行主线程。
(1.3)detach():分离,主线程和子线程分离,各自执行各自的。引入原因:创建很多子线程,让主线程逐个等待子线程结束,不太好,所以引入detach()。
一旦detach()之后,与这个主线程关联的thread对象就会失去与这个主线程的关联。此时子线程就会在后台运行,当子线程执行完毕后,又运行时库负责清理相关的资源(守护线程)。 如:myobj.detach();
detach()使线程失去控制。一旦使用detach()后,不能再用join()了。
(1.4)joinable():判断时候可以成功使用join()或者detach();
2.其他创建线程的方法:函数,类,lambda表达式
(2.1)用类:重载括号 operator()(); thread myobj(对象名,参数);
调用成员函数: thread myobj(&类型:成员函数名,对象名,参数);
*调用detach时,主线程结束后,这个对象不存在了,但是这个对象会被复制到线程中去。只要类对象中没有引用,指针时,不会产生问题。
*无论使用detach还是join,都会执行拷贝构造函数。
*Ta ta; thread myobj(ta); 先执行构造函数,接着拷贝构造函数,然后主线程执行ta的析构函数,子线程中执行ta的对象的(),最终子线程执行ta的析构函数。
(2.2)用lambda表达式:auto mylamthread = []{ ;} thread myobj(mylamthread); myobj.join();
三、线程传参详解,detach()大坑,成员函数做线程函数
1.传递临时对象作为线程参数
(1.1)要避免的陷阱(解释1) thread myobj(函数名,参数1,参数2,……);
使用引用时,子线程也会进行值传递,但是指针进行值传递,也是指向相同的内存。当使用detach时,最好不使用指针和引用。
(1.2)要避免的陷阱(解释2)
有bug:char mybuf[] = “this is a test”; thread myobj(myprint,mybuf); myprint(string &mybuf); 事实上存在,mybuf被回收了,系统才会用mybuf去转string。
thread myobj(myprint,mybuf); --> thread myobj(myprint,string(mybuf));改成临时变量就没有问题了。改了之后在主线程转换。
在创建线程的同时构造临时对象的方法传递参数是可行的。
(1.3)总结
a.若传递简单类型参数,建议使用值传递,尽量不使用引用。
b.如果传递类对象,避免隐式类型转换,全部都在创建线程这一行就构建出临时对象,然后在进行函数参数里用引用来接,否则还会构造一次对象(3次)。
c.不使用detach,就不会存在局部变量失效问题和对内存的非法使用问题。
在函数中隐式转换,是在子线程中进行的,创建线程是显示转换,在主线程中进行的,拷贝构造函数也是在主线程完成的。
可以这样理解:在创建子线程后,主线程会拷贝一个临时变量供子线程使用,所以子线程如果使用引用,则引用临时变量,不使用引用还会多进行一次值传递。
2.临时对象作为线程参数继续讲
(2.1)线程id概念:每个线程实际上都对应一个不同的ID号。通过std::this_thread::get_id();获取。
(2.2)临时对象构造时机抓捕
3.传递类对象、智能指针作为线程参数:尽量不要使用detach();
std::ref(); 例:thread myobj(myprint,std::ref(value));使用ref才能真正传递引用,才能在函数中不使用const。或者在创建线程是使用&变量。
thread myobj(myprint,std::move(smart_p));
4.用成员函数指针做线程函数:
thread myobj(&A::thread_work,a,value); 参数列表(&类型::成员函数,对象名,值);
四、创建多个线程,数据共享问题分析、案例分析
1.创建和等待多个线程
a)多个线程执行顺序是乱的,跟操作系统内部对运行调度有关。
b)都使用join,则主线程等待所有子线程运行结束。
c)把thread对象放入容器里,对线程的管理和操作比较方便。
2.数据问题共享分析
(2.1)只读的数据:数据只读所有线程读到的数据一样,是安全稳定的,直接读就可以。
(2.2)有读有写:最简单处理:读的时候不能写,写的时候不能读,两个线程不能同时写。
(2.3)其他案例
3.共享数据的保护案例代码
五、互斥量概念、用法、死锁演示及解决方法
保护共享数据,操作室,用代码把共享数据锁住,操作数据、解锁,其他想操作共享数据的线程必须等待解锁,锁住、操作、解锁。
1.互斥量(mutex)的基本概念
互斥量就是个类对象,可以理解为一把锁,多个线程尝试用lock()成员函数来加锁,只有一个线程能锁定成功,如果没有锁成功,那么流程将卡在lock()这里不断尝试去锁定。
互斥量使用要小心,保护数据不多也不少,少了达不到效果,多了影响效率。
2.互斥量的用法,头文件 std::mutex my_mutex;
(2.1)lock(),unlock():先lock(),操作共享数据,unlock()。
lock()和unlock()要成对使用,有lock()必然有unlock(),调用一次lock(),必须调用一次unlock()。非对称数量使用,都会使代码不稳定。
(2.2)std::lock_guard类模板:std::lock_guardstd::mutex my_guard(mutex); lock_guard构造函数执行了mutex::lock();在作用域结束时,调用析构函数,执行mutex::unlock(); 用lock_guard取代lock()和unlock();
3.死锁
(3.1)死锁演示
死锁至少有两个互斥量mutex1,mutex2。
a.线程A执行时,这个线程先锁mutex1,并且锁成功了,然后去锁mutex2的时候,出现了上下文切换。
b.线程B执行,这个线程先锁mutex2,因为mutex2没有被锁,即mutex2可以被锁成功,然后线程B要去锁mutex1.
c.此时,死锁产生了,A锁着mutex1,需要锁mutex2,B锁着mutex2,需要锁mutex1,两个线程没办法继续运行下去。。。
(3.2)死锁的一般解决方案:只要保证多个互斥量上锁的顺序一样就不会造成死锁。
(3.3)std::lock()函数模板:std::lock(mutex1,mutex2……); 一次锁定多个互斥量,用于处理多个互斥量。一次锁住两个或者两个以上的互斥量。它不存在在多个线程中,因为锁的顺序问题产生的死锁问题。
如果互斥量中一个没锁住,它就等着,等所有互斥量都锁住,才能继续执行。如果有一个没锁住,就会把已经锁住的释放掉(要么互斥量都锁住,要么都没锁住,防止死锁)
(3.4)std::lock_guard的std::adopt_lock参数
std::lock_guardstd::mutex my_guard(my_mutex,std::adopt_lock); 加入adopt_lock后,在调用lock_guard的构造函数时,不再进行lock();
adopt_guard为结构体对象,起一个标记作用,表示这个互斥量已经lock(),不需要在lock()。
六、unique_lock(类模板)详解
1.unique_lock取代lock_guard
unique_lock比lock_guard灵活很多,效率差一点。
std::unique_lockstd::mutex myunique_lock(my_mutex);
2.unique_lock的第二个参数:std::unique_lockstd::mutex my_unique_lock(my_mutex,adopt_lock);
(2.1)std::adopt_lock:表示这个互斥量已经被lock(),必须提前将互斥量lock了,即不需要在构造函数中lock这个互斥量了。
(2.2)std::try_to_lock:尝试用mutx的lock()去锁定这个mutex,但如果没有锁定成功,会立即返回,不会阻塞在那里;用try_to_lock的前提是你自己不能先去lock();
std::unique_lockstd::mutex my_unique_lock(my_mutex,std::try_to_lock); 可以用my_unique_lock.owns_locks()判断是否拿到锁。不会阻塞卡主。
(2.3)std::defer_lock:前提:不能先lock,否则会报异常。defer_lock()的意思是美誉给mutex加锁,初始化了一个没有加锁的mutex。使用lock()和unlock()加锁解锁。
3.unique_lock的成员函数(与std::defer_lock联合使用)
(3.1)lock():加锁。
std::unique_lockstd::mutex my_unique_lock(my_mutex,std::defer_lock); my_unique_lock.lock(); 不用自己unlock();
(3.2)unlock():解锁。my_unique_lock.unlock();因为一些非共享代码要处理,可以先unlock(),处理完后再lock(); 多写unlock()也没问题,会自动判断。
(3.3)try_lock():尝试给互斥量加锁,如果拿不到锁,返回false,否则返回true。这个函数不阻塞。my_unique_lock.try_lock()返回true和false。
(3.4)release():返回它锁管理的mutex对象指针,并释放所有权;也就是说,这个unique_lock和mutex不在有关系。
如果原来mutex对象处理加锁状态,有责任解锁这个mutex。
lock的代码段越少,执行越快,整个程序的运行效率越高。
a.锁住的代码少,这个粒度叫细,执行效率高;
b.锁住的代码多,粒度粗,执行效率低。
4.unique_lock所有权的传递
std::unique_lockstd::mutex my_unique_lock(my_mutex); 所有权概念
a.使用move转移。
my_unique_lock拥有my_mutex的所有权,my_unique_lock可以把自己对my_mutex的所有权转移,但是不能复制。
std::unique_lockstd::mutex my_unique_lock_1(std::move(my_unique_lock)); 现在my_unique_lock_1拥有my_mutex的所有权。
b.在函数中return一个临时变量,即可以实现转移。
七、单例设计模式共享数据分析、解决,call_once
1.设计模式
2.单例设计模式:整个项目中,有某个或者某些特殊的类,只能创建一个属于该类的对象。单例类:只能生成一个对象。
例:class MyCAS{ private: MyCAS();//私有化构造函数; static MyCAS *m_instance; //静态成员变量。public: static MyCAS *GetInstance(){ if(m_instance == NULL) {m_instance =new MyCAS();static CGarhuishou cl;} return m_instance;} class CGarhuishou{~CGarhuishou(){if(MyCAS::m_instance){delete MyCAS::m_instance;MyCAS::m_instance = NULL;}}}} MyCAS *MyCAS::m_instance =NULL; 类静态变量初始化
MyCAS *p_a = MyCAS::GetInstance();//创建一个类对象。
3.单例设计模式共享数据分析、解决
面临问题:需要在自己创建的线程中来创建单例类的对象,这种线程可能不止一个。我们可能面临GetInstance()这种成员函数需要互斥。
可以在加锁前加入判断,m_instance是否为空。
4.std::call_once():函数模板,该函数的第一个参数为标记,第二个参数是一个函数名(如a)。功能:能够保证函数(a)只被调用一次。具备互斥量的能力,而且比互斥量小号的资源更少。
call_once()需要与一个标记结合使用,这个标记std::once_flag;其实once_flag是一个结构,call_once()就是通过标记来决定函数是否执行,调用成功后,就把标记设置为一种已调用状态。
例:std::once_flag g_flag;call_once(g_flag,a); 多个线程同时执行时,一个线程会等待另一个线程先执行。
八、condition_variable、wait、notify_one、notify_all
1.条件变量std::condition_variable、wait()、notifiy_one()
线程A:等待一个条件满足
线程B:专门往消息队列中扔消息数据。
条件变量std::condition_variable实际上是一个类,是一个和条件相关的类,这个类和互斥量配合工作,需要生成类的对象。
例:std::condition_variable my_cv; my_cv.wait(my_unique_lock,[this]){if() return ture;else return false});
wait()用来等待一个东西,如果第二个参数lambda表达式返回值是false,那么wait()将解锁互斥量,并堵塞本行,堵塞知道其他某个线程调用notify_one()成员函数位置。
如果第二个参数是ture,那么wait()直接返回。
如果wait()没有第二个参数,那么跟第二个参数lambda表达式返回false效果一样。
my_cv.notify_one(); 尝试把wait()的线程唤醒,执行完这行,那么wait()就会被唤醒。
当其他线程将wait()(原本是睡着/堵塞状态)唤醒后,wait()就开始恢复执行:
a.wait()不断尝试获取互斥量,如果获取不到,那么流程就会卡在wait()这里等待获取,如果获取到(就会上锁),那么wait就继续执行b。
b.如果wait有第二参数,就判断这个参数,如果表达式为false,那么wait又对互斥量解锁,然后又休眠等待再次被notify_one唤醒。
如果wait没有第二个参数或者第二个参数表达式为ture,流程继续往下走(互斥锁被锁着状态)。
当另一个线程没有卡在wait那里,而是执行其他的事务,那么notify_one()就没有效果唤醒。
理解: 条件变量std::condition_variable、wait()、notifiy_one()可以在一个线程处理数据的频率不高,就不需要一直去尝试拿锁,知道等待notify_one通知。
2.上述代码深入考虑
3.notify_all()
notify_one():只能通知一个线程,如果有多个线程时,也只会激活一个线程。
notify_all():通知所有现场,激活所有wait的线程。
九、async、future、packaged_task、promise
1.std::async、std::future创建后台任务并返回值:头文件
希望线程返回一个结果,std::ansync是个函数模板,用来启动一个异步任务,启动起来一个异步任务之后,返回一个std::future对象,std::future是个类模板。
启动一个异步任务:自动创建一个线程并执行对应的线程入口函数,返回一个std::future对象,这个std::future对象里边就含有线程入口函数所返回的结果(线程返回的结果),可以通过future对象的成员函数get()来获取。
future:提供了一种访问异步操作结果的机制,就是说这个结果没有办法马上拿到,但是在这个线程执行完毕的时候,能够拿到结果。可以这样理解:这个future(对象)会保存一个值,在将来能够拿到。
std::future<类型> 变量 = std::async(线程函数名); 结果:变量.get();//程序会卡在get这儿,等待异步线程返回。 变量.wait();//等待线程返回,但是不返回结果。
通过std::future对象的get()成员函数等待线程结束并返回结果,get不拿到返回值,就会一直等待,get只能调用一次,get()是移动语义,不能多次调用。
std::async(&类名:成员函数,&类对象,参数列表); //第二个参数是对象引用,才能保证线程里用的是同一个对象。
如果不用wait()或者get(),程序也会等待,才会结束主线程。
我们通过额外向std::async()传递一个参数,该参数类型是std::launch类型(枚举类型),来达到一些特殊目的;std::async(std::launch::类型,&类名:成员函数,&类对象,参数列表);
a。std::launch::deferred:表示线程入口函数调用被延迟到std::future的wait()或get()函数调用才执行,并且线程不会被创建,在主线程中调用线程入口函数的。如果不调用wait或者get,则线程不被创建也就不会被执行。
b。std::launch::async:在调用async时就开始创建新线程,并执行。默认就是用的这个参数。
c。std::launch::deferred | std::launch::async 同时使用时,行为是不确定的,可能是deferred(没有创建新线程,并且延迟调用)或者async(强制创建新线程并立即执行)。
d。不带额外参数,默认值为std::launch::deferred | std::launch::async;系统会自行决定是异步方式(创建新线程)还是同步方式(不创建新线程)执行。
2.std::packaged_task
std::packaged_task:类模板。它的模板参数是可调用对象。把各种可调用对象包装起来,方便将来作为线程入口函数调用。
例:std::packaged_task<返回类型(函数参数类型)> mypt(函数名); //把函数通过packaged_task包装起来; std::thread t1(std::ref(mypt),1); //线程开始执行,第二个参数为线程入口函数的参数 t1.join;或者可以直接调用mypt(1),这样在主线程执行。
std::future result = mypt.get_future(); //std::future对象里包含有线程入口函数的返回值,这里result保存mythread的返回值。
例:std::packaged_task<返回类型(函数参数类型)> mypt(lambda表达式);
std::packaged_task包装起来的可调用对象还可以直接调用,从这个角度讲,std::packaged_task对象可以直接调用。
3.std::promise
类模板,能够在某个线程中给他赋值,然后可以在其他线程中,把这个值取出来。
void mythread(std::promise &tmp, int calc) { int reslut = calc; tmp.set_value(result);//结果保存到tmp这个对象中。}
main:std::promise mypro;//声名一个promise对象,保存的值为int类型
std::thread t1(mythread,std::ref(mypro),180); t1.join; 获取结果:std::future ful = mypro.get_future();//promise和future绑定,用于获取返回值。auto result = ful.get();
通过promise保存一个值,在将来某个时刻把一个future绑定到这个promise上来得到这个绑定的值。
4.小结
十、future其他成员函数、shared_future、atomic
1.std::future的其他成员函数
std::future_status status = result.wait_for(std::chrono::seconds(6));等待6秒钟。
std::future_status:枚举类型:timeout超时(表示线程还没有执行完,等待时间不够,比如等待1秒钟,然后需要5秒钟才能执行完);ready(表示线程成功返回。);deferred(延迟:如果async的第一个参数被设置为async时)
2.std::shared_future
因为get()是移动语义,只能调用一次,shared_future是类模板。例,使用future:std::future result = mypt.get_future(); std::shared_future result_s(std::move(result)); std::shared_future result_s(result.share()); //执行完毕后result_s里有值,而result的值为空了
可以用个result.valid()判断result是否有值。例,不通过future:std::shared_future result_s(mypt.getfuture()); 通过get_future返回值直接构造shared_future对象。
3.原子操作std::atomic
(3.1)原子操作概念引出范例
互斥量:多线程编程中,保护共享数据,锁,操作共享数据,开锁。
问题:两个线程,对一个变量进行操作,这个线程读变量值,另一个线程往这个变量中写值,可能读取的值为莫名其妙的值,也可以说多个线程同时操作同一个变量,这种操作不安全。
解决:方法1:可以使用互斥量。
方法2:原子操作。
原子操作理解:不需要用到互斥量(无锁)技术的多线程并发编程方式,也可以理解成,在多线程中不会被打断的程序片段,原子操作比互斥量效率更高。
互斥量的加锁一般针对一个代码段(多行代码),而原子操作一般都是针对一个变量操作。
原子操作,一般指不可分割的操作,也就是说这种操作状态要么是完成的,要么是未完成的,不可能有半完成状态。
(3.2)基本的std::atomic用法范例
std::atomic:类模板,std::atomic用来封装摸一个类型的值的。
std::atomic my_atomic_int = 0; //封装了一个类型为int的对象(值),可以向操作一个int类型变量来操作这个对象。
(3.3)心得:原子操作一般用于技术或者统计(比如发送出去多少个数据包,累计收到多少个数据包)。
十一、std::atomic、std::async
1.原子操作std::atomic
一般atomic原子操作,针对++,–,+=,&=,|=,^=是支持的,其他的可能不支持。my_atomic_int ++;和my_atomic_int = my_atomic_int + 1;不等价。
2.std::async
(2.1)std::async参数详述,async是用来创建异步线程。
(a)如果用std::launch::deferred调用async,延迟调用future对象,当使用get或者wait时才会执行函数。如果不调用get或者wait时,不会调用函数。
(b)如果用std::launch::async调用async,强制这个异步任务在新线程上执行,系统必须创建出新线程来运行函数。
©std::launch::deferred | std::launch::async 同时使用时,行为是不确定的,可能是deferred(没有创建新线程,并且延迟调用)或者async(强制创建新线程并立即执行)。
(d)不带额外参数,默认值为std::launch::deferred | std::launch::async;系统会自行决定是异步方式(创建新线程)还是同步方式(不创建新线程)执行。
自行决定:
(2.2)std::async和std::thread的区别
std::thread():如果系统资源紧张,那么可能创建线程就会失败,那么执行std::thread()时整个程序可能崩溃。
如果线程返回值,拿到值不容易。
std::async():一般不叫创建线程,一般叫创建一个异步任务,可能创建线程,也可能不创建线程。并且async很容易拿到线程入口函数的返回值。
std::thread和std::async最明显的不同,就是async有时候不出创建新线程。
由于系统资源限制:
(a)如果用std::thread创建的线程太多,则可能创建失败,系统报异常,崩溃。
(b)如果用std::async,一般不会包异常,如果系统资源紧张导致无法创建新线程的时候,async这种不加额外参数的调用方式,就不会创建新线程,而是后续调用result.get()来请求结果,那么这个异步任务就运行在执行这条get()语句所在的线程上。
如果强制std::async创建新线程,用async标志,但是线程系统资源紧张时,也会导致崩溃。
(c)一个程序,线程数不宜超过100-200。
(2.3)std::async不确定性问题解决
不加额外参数的std::async调用,让系统自行决定是否创建新线程。问题焦点在于:这种写法异步任务到底有没有推迟执行,std::future对象的wait_for函数。
十二、Windows临界区、其他各种mutex互斥量
1.Windows临界区:<windows.h> #define _WINDOWSJQ
#ifdef _WINDOWSJQ CRITICAL_SECTION my_winsec; #endif
#ifdef _WINDOWSJQ InitializeCriticalSection(my_winsec);//用临界区之前要先初始化 #endif
#ifdef _WINDOWSJQ EnterCriticalSection(&my_winsec); //进入临界区,相当于加锁 代码段……; LeaveCriticalSection(&my_winsec);//离开临界区,相当于解锁 #endif
2.多次进入临界区实验
在同一个线程(不同线程就会卡主等待)中,Windows中的相同临界区变量代表的临界区的进入(EnterCriticalSection(&my_winsec);)可以被多次调用,但是你调用的几次进入,就得调用几次离开。
在同一个线程,C++中不允许多次加锁同一个互斥量,否则报异常。
3.自动析构技术
C++:lock_guard
windows:可以写个类自动释放临界区:class CWinLock{public:CWinLock(CRITICAL_SECTION *pCritmp){my_winsec =pCritmp; EnterCriticalSection(my_winsec);} ~CWinLock(){LeaveCriticalSection(my_winsec)};} private:CRITICAL_SECTION *my_winsec;}
上述这种类RAII类,即资源获取即初始化。容器,智能指针属于这种类。
4.recursive_mutex递归的独占互斥量
std::mutex:独占互斥量,自己拿到锁时,别人lock不了。
recursive_mutex:递归的独占互斥量,可以lock,unlock,和mutex的用法类似。允许同一个线程,同一个互斥量多次被.lock()。recursive_mutex的效率比mutex低。
5.带超时的互斥量std::timed_mutex和std::recursive_timed_mutex
std::timed_mutex:带超时功能的独占互斥量,
try_lock_for():等待一段时间,如果拿到锁,或者等待超多时间没拿到锁,就继续执行。try_lock_for(一段时间),一段时间内拿到锁返回ture。
try_lock_until():参数是一个未来的时间点,在这个未来的时间没到的时间内,如果拿到了锁,就执行,如果时间到了,没拿到锁,流程也继续。
std::recursive_timed_mutex:带超时功能的递归独占互斥量,允许同一个线程多次获取这个互斥量。
十三、补充、线程池浅谈、数量谈、总结
1.补充
(1.1)虚假唤醒:notify_one或者notify_all唤醒wait()后,实际有些线程可能不满足唤醒的条件,就会造成虚假唤醒,可以再wait中再次进行判断解决虚假唤醒。
解决:wait中要有第二个参数(lambda),并且这个lambda中药正确判断要处理的公共数据是否存在。
(1.2)atomic:atm=0;auto atm2=atm; 不允许,定义时初始化操作不允许,拷贝赋值运算符也不允许。
load():以原子方式读atomic对象值。atm2(atm.load()),
store():以原子方式写atomic对象值。
2.浅谈线程池
(2.1)场景设想
服务器程序,–>客户端,每来一个客户端,就创建一个新线程为该客户提供服务。这种方式在客户端很多的时候行不通,并且程序稳定性有问题。
线程池:把一堆线程弄在一起,统一管理。这种统一管理调度,循环利用线程的方式就叫线程池。
(2.2)实现方式
在程序启动时,一次性创建一定数量的线程。
3.线程创建数量谈
(3.1)线程开的数量极限问题,一般来说2000个线程基本就是极限,在创建线程就崩溃。
(3.2)线程创建数量建议:
a.采用某些技术开发程序;比如:创建线程数量 = CPU数量,cpu数量2,cpu数量2+2。
b.创建多线程完成业务,一个线程等于一条执行通路。
c.一般线程数不要超过500个,尽量200个以内。
4.C++11多线程总结:Windows,Linux。