C++面向对象程序设计(第4版)-课件 CH9_第1页
C++面向对象程序设计(第4版)-课件 CH9_第2页
C++面向对象程序设计(第4版)-课件 CH9_第3页
C++面向对象程序设计(第4版)-课件 CH9_第4页
C++面向对象程序设计(第4版)-课件 CH9_第5页
已阅读5页,还剩38页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

第9章线程本章主要教学内容程序、进程和线程三者的基本概念和关系简单的Windows和unix

Api线程程序设计简单标准C++线程程序设计类与线程简单的线程同步程序程序设计本章教学重点理解线程、进程和程序的关系,掌握线程程序的基本结构和运行原理WindowsAPI线程的结构和程序设计标准C++线程函数的结构、调用方法和简单线程设计线程同步及程序设计:互斥锁、读写锁、信号量、条件变量的简单应用程序设计类与线程:类成员函数线程设计。教学难点线程运行原理、主线程与子线程运行关系控制方法线程同步,互斥锁、信号量、条件变量控制的线程同步程序设计9.1程序、进程和线程1、程序、进程和线程的概念程序(program)是用程序设计语言编写的用来完成特定任务的一组命令集合,以文件形式保存在各种存储设备中,是静态的。进程(process)是被装载到内存中处于运行状态的程序,是动态的,要经历从外存载入内存,在内存中运行,运行完成后从内存中清除的完整过程,这个过程称为进程的生命周期。线程(thread)是进程中执行运算的最小单位,是从一个进程中划分出来的更小指令集合,该指令集合能够被CPU作为一个独立单元进行调度和执行。多线程,如果一个进程内可以划分出多个线程,并允许在同一时间并行执行它们,就称为多线程。在多线程系统中,单个线程并不拥有系统资源,而是只拥有少量在运行过程中必不可少的资源(程序计数器,一组寄存器和栈),系统资源由同一个进程中的各线程共享,因此线程之间的通信简便,调度切换效率高。9.1程序、进程和线程2、程序、进程和线程的关系一个程序可以对应多个进程,一个进程中可以包括多个线程;反之,一个线程只对应一个进程,一个进程只能对应一个程序。9.1程序、进程和线程3、线程的发展和优势在早期计算机系统中,操作系统进行资源分配和独立调度执行的基本单位是进程。在单核CPU时代,这种程序执行方式并无大碍。对称处理机(SymmetricMulti-Processing,SMP,在一台计算机中汇集了一组处理器)和多核心CPU的出现,以进程为调度执行单位出现了许多弊端。一是进程作为资源拥有者,在创建、撤销与切换时存在较大的时空开销;二是由于多处理机可以同时满足多个运行单位,而多进程并行执行的开销过大,进程间切换的效率较低,于是在20世纪80年代出现了线程。线程优势:线程比进程更小,能够共享同一进程内的资源,线程间的调度切换代价小,速度快,可以在多核CPU中并发运行,提高了系统资源的利用率和吞吐量在四核心CPU系统中,每个CPU中以独立执行进程中的1个线程,4个线程。若以程为执行单位,则有CPU处于空闲状态。显然线程效率更高!9.1程序、进程和线程3、C++11标准之前的线程程序设计11标准之前的C++不支持线程,如果要在C++中进行线程设计,需要通过操作系统提供的线程API(ApplicationProgrammingInterface)函数才能够实现。主流操作系统又分为Linux和Windows两大系列,两者线程API并不相同。Linux线程设计在Linux的pthread.h头文件中,提供了创建线程的API函数:intpthread_create(pthread_t*thread, //返回创建的线程IDconstpthread_attr_t*attr,//设置线程属性,通常为NULL(系统默认属性)

void*(start_routine)(void*),

//设置线程函数

void*arg); //传入所设置的线程函数的参数9.1程序、进程和线程3、C++11标准之前的线程程序设计Windows线程设计在windows.h头文件中,提供了创建线程的API函数:HANDLECreateThread(LPSECURITY_ATTRIBUTESlpThreadAttributes,//系统安全属性,常取NULLSIZE_TdwStackSize,//线程栈大小,常设为0(表示用系统默认值)

LPTHREAD_START_ROUTINELpstartAddress, //设置线程函数

LPVOIDlpParameter,//传入设置的线程函数的参数

DWORDdwCreationFlags,//设置启动方式,常设为0(创建后立即启动)

LPDWORDlpThreadID//返回创建的线程ID);【例9-1】简单WindowsAPI线程设计。#include<windows.h> //线程API函数定义在此头文件中#include<stdio.h>DWORDCALLBACKWinThread(LPVOIDlpParamters){//L1,Windows线程

while(true){staticinti=0;Sleep(1000); //L2printf("%d秒过去了\n",++i);}return0;}intmain(){DWORDthreadID;HANDLEhthread=CreateThread(NULL,0,WinThread,NULL,0,&threadID); //L3if(hthread==NULL){ //L4printf("线程创建失败.\n");return-1;}printf("thread:%d\n",threadID);//L5Sleep(5000);//L6return0;}运行结果如下:thread:84561秒过去了2秒过去了3秒过去了4秒过去了5秒过去了9.1程序、进程和线程4、C++11标准的线程

Windows和Linux的API线程只能在对应的操作系统中运行,不具跨平台运行能力,并有设计麻烦。为解决上面的问题,c++11提供了线程标准,支持跨平台的线程设计。9.1程序、进程和线程5、C++线程设计方法可以在linux或windows平台建立的C++开发环境中,用C++标准提供的线程类设计线程程序,其设计方法相同。因为,C++11标准中的线程类thread具有跨平台运行能力,同一个线程程序可以在Linux和Windows操作系统中运行。基本步骤:#include<thread>头文件,要用其中的thread类创建线程对象定义线程函数(一个普通函数),如:funcname(typepara1,…,typeparan);调用线程类的构造函数创建线程对象:threadt(funcname,para1,…,paran);其中,thread是线程类,funcname是第2步定义的线程函数,para1、…、panrn是传递给线程函数funcname的调用实参。将线程与线程对象分离或加入线程阻塞对列(解决主线程main和子线程的生命期不同步问题,防止主线程生命期结束而子线程仍在运行所带来的问题。t.detach()t.join()9.1程序、进程和线程5、C++线程设计方法可以在linux或windows平台建立的C++开发环境中,用C++标准提供的线程类设计线程程序,其设计方法相同。因为,C++11标准中的线程类thread具有跨平台运行能力,同一个线程程序可以在Linux和Windows操作系统中运行。基本步骤:#include<thread>头文件,要用其中的thread类创建线程对象定义线程函数(一个普通函数),如:funcname(typepara1,…,typeparan);调用线程类的构造函数创建线程对象:threadt(funcname,para1,…,paran);其中,thread是线程类,funcname是第2步定义的线程函数,para1、…、panrn是传递给线程函数funcname的调用实参。将线程与线程对象分离或加入线程阻塞对列(解决主线程main和子线程的生命期不同步问题,防止主线程生命期结束而子线程仍在运行所带来的问题。t.detach()t.join()【例9-2】

用C++11的线程类thread创建无参线程函数thread1(

)和有参线程函数thread2(

)。#include<iostream>#include<thread>#include<Windows.h>usingnamespacestd;voidthread1(){//L1,普通无参函数

while(true){Sleep(1000);cout<<"thread1"<<endl;}}voidthread2(inta,intb){//L2,普通有参函数

while(true){Sleep(500);cout<<"thread2:"<<a<<"+"<<b<<"="<<a+b<<endl;}}intmain(){

std::threadt1(thread1);//L3,创建无参线程

std::threadt2(thread2,1,2);//L4,创建有参线程

Sleep(2000);//等待2秒钟//t1.detach();//L5,让线程与对象t1分离

//t2.detach();//L6,让线程与对象t2分离

return0;}程序运行输出结果:thread2:1+2=3thread1thread2:1+2=3thread2:1+2=3Endmain

//L7,主线程main结束thread1thread2:1+2=3thread2:1+2=3……

//L8,不断重复上面的输出terminatecalledwithoutanactiveexception程序运行2秒后因异常而崩溃!其原因是主线程main(

)在休眠2秒钟后会结束运行,线程对象t1和t2会因程序结束而被销毁,但绑定到线程对象t1和t2上的线程还会继续运行,因此产生错误。L7位置就是main线程结束时输出的,其后的输出表明main(

)结束后,子线程仍然在运行状态中解决办法之一:取消L5,L6的注释,让线程对象t1,t2与其绑定的子线程分离。9.2线程等待和线程ID获取线程对象与绑定线程生命期不等的解决办法用detch分离线程对象与其绑定的线程用join将线程加入阻塞队列,称为线程等待detach()和join()是线程类thread提的两个成员函数,主要用于处理主线程与子线程之间的控制流程,解决主线程结束了而子线程仍然在运行的问题。detach()的作用是将子线程与创建它的线程对象分离开来,以此避免线程对象与它拥有的线程具有不同生存期的矛盾,其用法如例9-2所示(语句L5和L6),但此方法可以产生主线程结束后,子线程仍在不停执行的问题。例如,在例9.2中,取消语句L5,L6的注释,子线程thread1,thread2与对应的线程对象t1,t2分离后,随着主线程main的结束,t1,t2对象也因失去作用域而被正常销毁。但线程thread1,thread2因与对应线程对象已经脱离绑定关系而独立运行,由于线程中存在死循环,所以会永远执行下去。9.2.1线程等待1、为什么要使用线程等待C++的线程由线程对象创建和管理,线程对象受到作用域和生命期控制,而线程实际上一种是一种普通程序,程序逻辑可能出现线程对象的生命期结束了但其绑定线程仍然在运行的情况。如前所述,让线程与线程对象脱离是一种解决办法,但可以出现不合理的线程执行逻辑,如例9-2所法。另一种常有的解决办法就是线程等待。线程等待可以简单理解为:将子线程加入让主线程等待的队列,即让子线程执行完成后主线程再继续执行的线程队列(一个阻塞主线程让其中的线程先执行的子线程队列,也称作线程阻塞)。在这种情况下,子线程可以安全地访问主线程中的资源,主线程则会等待子线程结束,回收子线程占用的系统资源后,再继续执行。2、线程等待(线程阻塞)、分离的编程方法用thread线程类的join()、detach()阻塞主线程,分离线程对象与其绑定线程。语法:threadt(treadfunc,para1…);//创建线程对象tt.joinable(); //判断t是否可以加入join队列,返回true或falset.join();//阻塞主线程,让主线程等待t优先执行t.detach();//分离t和它控制的线程,让线程独立说明:仅当子线程处于运行状态(可用joinable()判断),一个线程对象才能执行join或detach操作中的一个(两者不能同时执行),而且只允许调用一次。【例9-3】修改例9-2,让主线程等待子程结束后再退出。//Eg9-3.cpp#include<iostream>#include<thread>#include<Windows.h>usingnamespacestd;voidthread1(){while(true){Sleep(1000);cout<<"thread1"<<endl;}}voidthread2(inta,intb){while(true){Sleep(500);cout<<"thread2:"<<a<<"+"<<b<<"="<<a+b<<endl;}}intmain(){std::threadt1(thread1);std::threadt2(thread2,1,2);if(t1.joinable())

t1.join();//L1t1加入阻塞main线程队列if(t2.joinable())

t2.join();//L2

t2输入阻塞main队例,即让main等t2优先执行Sleep(2000);return0;}程序运行结果如下,thread2:1+2=3thread1thread2:1+2=3thread2:1+2=3thread1……

对比例9-2,程序不会异常中止。因线程死循环,执行永不停9.2.2

获取线程ID线程ID线程被创建后,系统会为它分配一个线程ID。这个ID在整个操作系统范围内是唯一的,在程序中可以用线程ID来识别不同的线程。获取方法用类this_thread中get_id()方法或类threadget_id()方法,如下所示:this_thread::get_id(); //static成员函数thread::get_id(); //非static成员函数注意事项this_thread::get_id()是一个静态成员函数,用于获取当前线程的ID,不需要创建对象就可以通过类this_thread直接引用它。Thread::get_id()是一个非静态成员函数,必须创建类thread的对象后,通过对象才能够引用它。get_id()获取的线程ID是一个封装好的类类型thread::id,可以用cout直接输出,但不能直接作为整数使用。如果需要将id作为整数使用,可以先将其转换成一个ostringstream类型的字符串输出流对象,再将此对象转换成字符串类型,最后才能够将该字符串转换成整数。【例9-4】设计线程thread1创建一个磁盘文件,并将自己的线程ID和一串字符写入文件,创建线程thread2读取thread1创建的文件内容,用静态和非静态的get_id()获取thread1的线程ID,并将主线程main的线程ID转换为整数#include<iostream>#include<thread>#include<fstream>#include<sstream>usingnamespacestd;

voidthread1(stringfilename){ofstreamoutfile(filename); //L1,创建磁盘文件outfile<<this_thread::get_id()<<"\t" //L2,在文件中写入线程ID和字符串<<"thread1writethisstring!"<<endl; cout<<"inthread1,ID:"<<this_thread::get_id()<<endl; //L3,输出当前线程的ID}intthread2(stringfilename){ifstreaminfile(filename); //L4,可打开thread1创建的文件chars[100]; cout<<"Iamthread2,thread1writethefollowingstring:"<<endl; //L5while(!infile.eof()){ //L6,读出thread1建立的文件数据infile.getline(s,100); cout<<s<<endl; //L7,输出文件中的内容}return1;}

intmain(){threadt1(thread1,"D:\\abc.txt");//L8,创建t1线程对象,建立abc.txt文件threadt2(thread2,"D:\\abc.txt"); //L9,创建t2线程对象,读取abc.txt文件cout<<"thread1ID:"<<t1.get_id()<<endl;//L10,获取t1线程对象的线程IDif(t1.joinable())t1.join();if(t2.joinable())t2.join();cout<<"mainthreadID:"<<this_thread::get_id()<<endl; //L11,输出主线程main的IDthread::idmid=std::this_thread::get_id(); //L12,获取主线程main的IDostringstreamoss;oss<<mid; //L13,转换主线程ID为字符串流对象std::stringstr=oss.str(); //L14,将ID对象转换为字符串IDstd::cout<<"mainthreadID:"<<str<<std::endl;unsignedlonglongthreadid=std::stoull(str); //L15,将字符串ID转换为数值型std::cout<<"mainthreadID:"<<threadid<<std::endl;return0;}执行程序,其中的一个输出结果如下:thread1ID:17608 //语句L10的输出Iamthread2,thread1writethefollowingstring://语句L5的输出17608thread1writethisstring! //语句L6、L7的输出inthread1,ID:17608 //语句L3的输出mainthreadID:18232mainthreadID:18232mainthreadID:182329.3类和线程类静态与非静态成员函数的形参区别classA{voidf1(inta,intb){…}staticvoidf2(inta,intb){…}}f1()和f2()被编译器处理后,其原型变成了如下形式:voidf1(A*this,inta,intb);voidf2(inta,intb);C++线程对象的构造参数要求threadt(func,p1,p2,…);如何向C++标准线程对象传递线程函数类非静态成员函数作线程threadt(&A::f1,this,a,b)//非静态成员函数要额外传this指针类静态成员函数作线程threadt(&A::f2,a,b)【例9-5】为类myThread设计线程成员函数Write(),将它的线程ID和一些字符串写入磁盘文件,设计static线程成员函数Read()读取并输出Write()创建的磁盘文件内容#include<fstream>#include<sstream>#include<thread>#include<iostream>usingnamespacestd;classmyThread{shared_ptr<thread>t1,t2;public:myThread(){t1=t2=nullptr;}~myThread(){}voidWrite(stringfilename){t1.reset(newthread(&myThread::thread1,this,filename));//L1if(t1->joinable())t1->join();}voidRead(stringfilename){t2.reset(newthread(&myThread::thread2,filename)); //L2if(t2->joinable())t2->join();}voidthread1(stringfilename){ofstreamoutfile(filename);outfile<<this_thread::get_id()<<"\t"<<"string1"<<endl;outfile<<this_thread::get_id()<<"\t"<<"string2"<<endl;outfile.close();}staticvoidthread2(stringfilename){ifstreaminfile(filename);chars[200];while(!infile.eof()){infile.getline(s,100);cout<<s<<endl;}}};intmain(){myThreadt;t.Write("D:\\abc.txt");t.Read("\D:\\abc.txt");return0;}执行程序后,结果如下:10760string110760string2shared_ptr<thread>t1,t2;MyThread用智能指针管理对象,因此不需要deletet1,t2!9.4线程同步为什么要进行线程同步假设多个线程需要操作同一资源,如读写同一个内存变量,修改同一个磁盘文件,使用同一台打印机,如果不加控制就会产生错误,因此需要避免这样的错误。线程同步是指在多个线程并发执行时,保证它们按照一定的顺序执行以达到正确的结果。因此,多线程必须进行线程同步设计。如何进行线程同步其基本思想是,当一个线程在对某内存区域进行写操作时,其他线程都不可以对这个内存区域进行操作,需要等到该线程完成对该内存区域的写操作并释放对它的控制后,其他线程才能够对该内存区域进行操作;如果所有线程执行的都是读操作,就可以同时执行线程同步的方法则是用互斥锁、信号量、条件变量、读写锁等技术对多线程共用的内存区域加以保护和使用控制,以避免多线程访问时所产生的冲突问题。9.4.1互斥锁互斥锁:mutexMutex的作用是对多线程共同访问的资源进行保护。主要成员如下:mutex::lock();mutex::unlock();mutex::try_lock();一个mutex在同一时刻最多只能属于一个线程,获取mutex的线程就成为它的拥有者,可以对mutex实施lock操作。等到该线程执行unlock操作后,其他线程才能获得该mutex。C++11标准中的其它常用互斥锁互斥锁类型标准说

明mutexC++11基本互斥锁timed_mutex C++11有限时机制的互斥锁recursive_mutex C++11能被同一线程递归锁定的互斥锁recursive_timed_mutexC++11timed_mutex和recursive_mutex双重特点的互斥锁shared_mutexC++17共享互斥锁shared_timed_mutexC++14有限时机制的共享互斥锁【例9-6】设计一个抢占教室座位号的程序,假设在3秒内,每名学生每次只可以占1个座位,但可以占座3次。#include<iostream>#include<windows.h>#include<thread>usingnamespacestd;intseatnum=0;voidoccuSeat(stringname){for(inti=0;i<3;i++){++seatnum;cout<<name<<"抢占了座位号:"<<seatnum<<endl;Sleep(3000);}}intmain(){threadt1(occuSeat,"张三");threadt2(occuSeat,"李四");t1.join();t2.join();return0;}线程函数occuSeat(

)模仿学生占座位的行为,它以学生姓名为参数,每调用一次函数就表示某学生的一次抢占座位行动。程序运行结果如下:李四抢占了座位号:

张三抢占了座位号:22张三抢占了座位号:4李四抢占了座位号:4李四张三抢占了座位号:6抢占了座位号:6同一座位被多次抢占!例9-7用互斥锁解决此问题!【例9-7】设计一个抢占教室座位号的程序,假设在3秒内,每名学生每次只可以占1个座位,可以占座3次,但同一次座位不允许被多次抢占。#include<mutex>#include<thread>#include<iostream>#include<windows.h>usingnamespacestd;intseatnum=0;mutexseatmux;//L1,定义互斥锁voidoccuSeat(stringname){for(inti=0;i<3;i++) {

seatmux.lock();//L2,锁住互斥锁

++seatnum;cout<<name<<"抢占了座位号:"<<seatnum<<endl;

seatmux.unlock();//L3,释放互斥锁

Sleep(3000);//L4,等待3秒钟

}}intmain(){threadt1(occuSeat,"张三");threadt2(occuSeat,"李四");t1.join();t2.join();return0;}程序运行结果:张三抢占了座位号:1李四抢占了座位号:2李四抢占了座位号:3张三抢占了座位号:4张三抢占了座位号:5李四抢占了座位号:69.4.2读写锁C++17读写锁:shared_mutexshared_mutex也称为“共享–独占锁”,允许多个线程同时以读模式加锁,但只允许一个线程以写模式加锁,并且读时不允许写、写时不允许读。mutex只有加锁或者不加锁两种状态互斥锁相比较,而且一次只允许一个线程加锁。显然shared_mutex允许线程具有更高的并发性。Shared_mutex用法shared_mutex锁有读模式加锁、写模式加锁和不加锁三种状态。由unique_lock(独占锁)和shared_lock(共享锁)两个对象来配合使用。某线程已经通过unique_lock获得了shared_mutex锁,那么其他线程就不能获得该锁,尝试获得此锁(读模式或写模式)的线程会被阻塞;当没有任何线程获得独占锁时,其他线程才能用shared_lock获得shared_mutex锁。此外,每个线程在同一时刻只能获得shared_mutex共享锁或独占锁中的一个unique_lock和shared_lock对象在被定义时会自动调用构造函数对shared_mutex加锁,在对象失去作用域时会自动调用析构函数对其锁住的shared_mutex对象解锁,在不需要可使用unlock操作对其锁住的shared_mutex对象解锁。【例9-8】用shared_mutex设计读写数据的线程,实现对同一内存数据的写入和同时读取功能。#include<iostream>#include<thread>#include<shared_mutex>usingnamespacestd;shared_mutexrwlock; //L1,定义读写锁intsharedata=0;//L2,共享内存区域voidreadData(stringtname){//L3,读线程函数

for(inti=0;i<12;i++){shared_lock<shared_mutex>rlock(rwlock);//L4,对rwlock申请读锁

cout<<tname<<"\tdata="<<sharedata<<endl;} //L5,自动释放rwlock的读锁}voidwriteData(stringtname){ /L6,写线程函数

for(inti=0;i<5;i++){unique_lock<shared_mutex>wlock(rwlock);//L7,对rwlock申请写锁

sharedata++;cout<<tname<<"\tdata="<<sharedata<<endl;} //L8,自动释放rwlock的写锁}intmain(){threadw1(writeData,"w1");threadr1(readData,"r1");threadr2(readData,"r2");threadr3(readData,"r3");r1.join();r2.join();r3.join();w1.join();return0;}某次运行的部分输出如下:w1data=1r1data=1r2data=1r3data=1r2data=1r1data=1r1data=1r1r3r2data=data=data=111

r3data=1r1data=r21data=1

w1data=2r3data=2r1r2data=data=22……9.4.3信号量c++20信号量的概念和功能信号量本质上是一个非负的整数计数器,具有P、V两种操作,一次P操作使信号量减1,一次V操作使信号量加1。主要用来控制多线程(进程)对公共资源的访问,限制并发访问共享资源的线程数量,被广泛应用于线程(进程)之间的同步和互斥控制中。其控制方法是,当信号量值大于0时,线程被执行,否则被阻塞(线程被挂起,直到信号量大于0)。C++标准中信号类的类型类型主要操作说

明counting_semaphorebinary_semaphoreacquire()执行p操作。若信号量大于0,则减少1,线程继续;否则阻塞线程,直到信号量大于0再唤醒继续执行release(n)执行v操作,信号量加n(省略n,则加1)9.4.3信号量c++203.信号量的用法包含头文件,必须是支持C++20标准及之后的C++编译器版本#include<semaphore>定义信号量:counting_semaphore<N>csem(信号量初始数量);//N是信号量上限值counting_semaphorecsem(信号量初始数量);//信号量无上限值binary_semaphorebsem(初值);//初值只能够是0或1【例9-9】设计1个线程函数,可以通过信号量同时启动5个线程,最多10线程。//Eg9-9.cpp#include<iostream>#include<semaphore>#include<thread>usingnamespacestd;counting_semaphore<10>csem(5);//L1intcakeNumber=0;binary_semaphorebsem(0);//示例二值信号量的定义方法voidmakeCake(stringthreadname){

csem.acquire(); //L2cout<<threadname<<"\tmakeCake“<<++cakeNumber<<endl;//csem.release(); //L3}intmain(){cout<<"main:readytosignal:release\n";threadt1=thread(makeCake,"bakeA:");//L4threadt2=thread(makeCake,"bakeB:");//L5threadt3=thread(makeCake,"bakeC:");//L6cout<<"main:signalend\n";

t1.join();t2.join();t3.join();return0;}程序运行结果如下:main:readytosignal:releasemain:signalendbakeC:makeCakebakeA:makeCake2bakeB:makeCake3

1L1定义了信号量csem为5,L2使信号量减1,L3处的信号量加1操作被注释,因此线程未释放资源。t1、t2、t3每执行一次,csem的信号量减1,因为初始值为5,减3之后仍然大于0,所以程序正常运行。从输出结果的第三行可以看到,在同一输出行中有3个线程的交叉输出,表明它们是并发执行的。【例9-10】信号量小于0时,线程被阻塞。在例9-9中,将L1语句中csem信号量的初值改为2,即修改成如下语句:counting_semaphore<10>csem(2);3个线程并发执行将使信号小于0,线程被阻塞,运行结果如下:无t2的输出【例9-11】信号量充足,阻塞线程被激活。在例9-10中,在主线程main中增加csem信号量,保障线程t1、t2、t3有充足的资源得以执行完成。

修改后的main()函数如下,其余程序代码不作修改。//Eg9-11.cpp……intmain(){cout<<"main:readytosignal:release\n";threadt1=thread(makeCake,"bakeA:");threadt2=thread(makeCake,"bakeB:");threadt3=thread(makeCake,"bakeC:");cout<<"main:signalend\n";csem.release(3);//L3释放资源,csem信号量加3t1.join();t2.join();t3.join();retrun0;}在例9-10中,初始信号量csem为2,t1,t2,t3三个线程对象中的线程执行将使信号量小于0,导到某个线程被阻塞。本例在例9-10的main主线程中,语句L3增加了3个信号量,从上面的输出结果可以分析出:3个线程都正常执行了!4.典型应用1:通过信号量,控制线程顺序执行【例9-12】设计线程函数,通过信号量控制多个线程依次执行(线性执行,无并发性)。解题思路:修改前面的程序,只需将信号量csem的初值设置为1,同时当线程获得资源并使用后,就立即释放它使信号量加1。9.4.3信号量c++20#include<iostream>#include<semaphore>#include<thread>usingnamespacestd;

counting_semaphorecsem(1);

//L1信号量初值为1intcakeNumber=0;voidmakeCake(stringthreadname){

csem.acquire();

//L2信号量减1cout<<threadname<<"\tmakeCake"<<++cakeNumber<<endl;

csem.release();

//L3信号量加1}intmain(){cout<<"main:readytosignal:release\n";threadt1=thread(makeCake,"bakeA:"); //L4threadt2=thread(makeCake,"bakeB:"); //L5threadt3=thread(makeCake,"bakeC:"); //L6cout<<"main:signalend\n";

t1.join();t2.join();t3.join();return0;}程序运行结果如下:main:readytosignal:releasemain:signalendbakeB:makeCake1bakeA:makeCake2bakeC:makeCake3由于信号量1,1次只有一个线程能够获取信号csem,当其获取后执行了csem.acquire()操作,信号变为0,至使其它线程被阻塞。线程执行完成后,通过csem.release()操作使信号量加1,变为1.某个线程再次获取信号量,得以执行4.典型应用2:通过信号量建立生产者-消费者线程模型可以用信号量控制多线程模仿于生产者—消费者模型。信号量代表产品数量,当生产者线程(增加信号量的线程)完成产品生产后,调用release()操作增加信号量,表示资源数量增加,可以根据当前资源的数量按需要唤醒指定数量的资源消费者线程(执行acquire减少信号量的线程)。资源消费者线程一旦获得信号量,就会减少资源数量,如果资源数量减少到0,那么消费者线程将全部处于阻塞状态;当有新资源到来时,消费者线程将继续被唤醒。9.4.3信号量c++20【例9-13】3个面包师,每次烤1个面包;2个顾客,每次消费1个面包。设计线程,模仿这个过程。设计思路:设计代表面包编号的全局变量cakeNumber,每烤1个面包就让cakeNumber加1,每消费1个面包就减1;设计一个counting_semaphore类型的信号量、一个代表生产面包的线程函数makeCake()和一个代表消费面包的线程函数consumerCake();每生产1个面包就调用release操作,让信号量加1;每消费1个面包就调用acquire操作,让信号量减1。9.4.3信号量c++209.4.3信号量c++20//Eg9-13.cpp#include<iostream>#include<semaphore>#include<thread>usingnamespacestd;counting_semaphorecsem(1);//信号量初始为1,也可为0intcakeNumber=0;voidmakeCake(stringthreadname){cout<<threadname<<":makeCake"<<++cakeNumber<<"\t"<<endl;

csem.release();//信号量加1}voidconsumerCake(stringthreadname){

csem.acquire();//信号量减1cout<<threadname<<"consumerCake"<<cakeNumber--<<"\t"<<endl;}intmain(){cout<<"main:readytosignal:release\n";threadt1=thread(makeCake,"bakeA:");threadt2=thread(makeCake,"bakeB:");threadt3=thread(makeCake,"bakeC:"); threadt4=thread(consumerCake,"customer1:");threadt5=thread(consumerCake,"customer2:"); cout<<"main:signalend\n";

t1.join();t2.join();t3.join();t4.join();t5.join();}每次结果不尽相同,某次结果如下:main:readytosignal:releasemain:signalendbakeA::makeCake1bakeC::makeCake2bakeB::makeCake3customer1:consumerCake3customer2:consumerCake29.4.4条件变量为什么会用条件变量在线程设计中存在这样一种业务逻辑,就是线程A通过无限次循环检测某个条件的成立,如果条件不成立就一直检测,直到条件成立时才能够执行其他业务逻辑,而条件是由另一个线程B修改的。两个线程共同应用的条件可以用信号量来抽象,如图所示。线程A存在较大的效率问题!可能存在这样一种情况:信号不大于0,但在它刚好释放互斥锁之后,线程B增加了信号量,它也会睡眠n秒钟,等到下次进行条件检测时才会退出循环。条件变量就是用于解决线程A的效率问题的:9.4.4条件变量条件变量工作原理如果线程A在检查到条件不满足(信号量不大于0)时,就立即释放互斥锁并处于等待状态(在图中的wait处等待);线程B在增加了信号量后(图中的notify())就主动通知线程A,并让出互斥锁,A就不用在每次条件不符合时都等待n秒钟,而是条件一旦满足就马上执行,最大限度地提高运行效率。9.4.4条件变量3.条件变量编程方法#include<condition_variable>应用condition_variable或condition_variable_any条件变量的成员函数wait()和notify()与互斥锁配合实现对线程的条件控制:wait(unique_lock<mutex>&lck)(1)wait(unique_lock<mutex>&lck,Predicatepred)(2)notify_one()notify_all()//通知全部函数wait()的功能是阻塞当前线程,直到收到notify()的通知为止。在第(2)种用法中,当pred=false时,阻塞线程;当pred=true时,不阻塞线程。wait()可依次拆分为三个操作步骤:①释放互斥锁(lck);②等待在条件变量上;③再次获取互斥锁(当条件变量接到notify()的通知时,将再次获取lcx)。notify_one()只唤醒一个线程,不存在锁争用问题,所以被唤醒的线程能够立即获得锁。其余线程则会继续被阻塞,等待再次被notify_one()或者notify_all()唤醒。notify_all()会唤醒所有被阻塞的线程,因此存在锁的争用,只有一个线程能够获得锁,其余未获得锁的线程会继续被阻塞,当持有锁的线程释放锁时,这些线程会继续尝试获得锁。9.4.4条件变量【例9-14】设计一个面包销售的生产者–消费者程序。生产者不断地生产面包并放入销售队列中,消费者从队列中取出面包,并执行面包销售任务。设计思路:在生产者–消费者模型中,通常由生产者产生任务后将其放入任务队列,然后通知消费者从任务队列中取出一个任务并予以执行。利用条件变量和互斥锁,可以便捷地实现这类程序模型。方法:让生产者线程和消费者线程通过互斥锁共同维护任务队列,实现两个线程对任务队列的异步访问,即:生产者线程获取互斥锁后将创建的任务放入任务队列,然后释放互斥锁并通知消费者线程到

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论