【文档说明】C--程序设计-现代方法chp10-多线程.pptx,共(30)页,427.305 KB,由小橙橙上传
转载请保留链接:https://www.ichengzhen.cn/view-2198.html
以下为本文档部分文字说明:
第十章多线程第十章多线程210.1案例-顺序执行的局限10.1.1案例的设计与实现•案例在程序做其它的事情的同时,在屏幕上显示一个实时时钟。•设计和实现可以将“其它的事情”编码为一个函数,显示实时时钟编码为另一个函数,然
后…然而,如果按照常规的编码模式,那么就不可能有然后了,因为常规模式并不支持同时做多件事。第十章多线程310.1案例-类型带来的困扰10.1.1案例的设计与实现size_tcounter=0;template<typenameT>voidcount(T&&v){++cou
nter;}//v未使用,可能导致编译警告intmain(){linked_list<int>l{1,2,3,4,5,6};l.traverse(count<int>);std::cout<<"thelisthas"<<counter<<"elements"<<std
::endl;return0;}将实际的函数作为回调传递个traverse。使用回调函数有哪些问题呢?可能会因需编写太多的回调函数。程序员对代码的控制较弱。此例还用到了全局变量。第十章多线程410.1案例-类型带来的困扰10.1.2案例问题分析同步Synch
ronizationf(){…g();…}g(){…return;}停—等第十章多线程510.2关键概念10.2.1异步异步Asynchronizationf(){…g();…}g(){…return;}第十章
多线程610.2关键概念10.2.2进程和线程进程(process)是正在运行的程序的一个实例。每一个进程都有它自己的地址空间,以及与运行相关的各种系统资源。多个进程可以并发执行。一个进程是可调度的,调度的工作由系统来完成。被调度的进程可以处于三种状态之一:运行态(running)、就绪态(re
ady)和阻塞态(blocked)。处于运行态的进程占据CPU资源。处于阻塞态的进程不占据CPU资源。第十章多线程710.2关键概念10.2.2进程和线程线程(thread)是进程中的实体,是被系统单独调度
的基本单位。除了运行时必备的资源外,线程一般不占有系统资源,但所有线程共享所属进程的资源。一个进程至少有一个线程。线程可以由另一个线程创建,并可以所属进程中的其它线程并发执行。与进程一样,线程也有同样的三种状态:运行态、就绪态和阻塞态。第十章多线程810.2关键概念10.2
.3互斥和共享readdthreadA:++dwritedreaddthreadB:--dwritedtime一种顺序读写操作readdthreadA:++dwritedreaddthreadB:--dwritedtime
另一种顺序的读写操作d=0d=-1d=0d=1数据竞争Datarace数据竞争必然带来不确定性问题。同一时间只能有一个线程独占共享数据访问权的排他性操作称为互斥(mutualexclusion)。第十章多线程910.2关键概念10.2.4锁和死锁要实现互斥,需要利用锁(lock
)机制。常见的锁分为排他锁(exclusivelock)和共享锁(sharedlock)。•排他锁用于互斥操作。如果线程A对资源d成功地加上了排他锁,那么称A拥有这把锁。此种情况下,系统会拒绝其它所有加锁申请,并阻塞申请者,直到A解锁(unlock)为止。此后,被阻塞的
线程(中的一个或一些)将得到调度。•共享锁用于共享操作。如果线程A对资源d成功地加上了共享锁,那么其它所有的加共享锁的申请都会被接受;但加排他锁的申请将会被拒绝,直到A解锁为止。第十章多线程1010.2关键概念10.2.4锁和死锁•通过仔细安排程序的逻辑,同时利用锁机制,可以确保线程的竞
争是可控制的,从而避免了数据的混乱。•但是,如果安排不当,或者发生了不可控的意外,那么可能导致更加严重的问题:死锁(deadlock)。例如,线程A对d加上了排他锁,但在解锁之前A被杀死(kill)了,那么d上的锁将
永久存在,其他的申请者将永远处于阻塞态得不到调度。这显然是不合理的。•由于程序的逻辑可能很复杂,因此预防死锁的发生代价较高。常见的处理方法是允许死锁的发生,但系统会介入处理。例如:如果系统发现了死锁,并在预定时间内没有解锁,那么系统会强制解锁。第十章多
线程1110.3C++的多线程库10.3.1头文件<thread>10.3.1.1thread类•thread类是多线程程序设计的基础,最常用的是它的一个构造函数:template<typenameF,typename...Args>explicitthread(F&&f,Args&&.
..args);•这是线程类的一个成员模板。其中,参数f是线程的执行样板,可以是任意满足条件的可调用对象,最常见的情况是一个函数;Args是可调用对象的模板参数包;args是传递给该可调用对象的参数。第十章多线程1
210.3C++的多线程库10.3.1头文件<thread>10.3.1.1thread类•thread类模板的实例化将会创建一个线程对象(threadobject)。例如:voidf(){…}//线程的执行样板std::thre
adt(f);•上述定义创建了一个名为t的线程对象,它唯一代表了一个新的执行线程(threadofexecution),该执行线程的执行路线以函数f为样板。第十章多线程1310.3C++的多线程库10.3.1头文件<thread>10.3.1.1thread类•被线程对象代
表的执行线程具有可结合(joinable)属性。一般情况下,新创建的线程要结合(join)到创建者线程中。这意味着,后者要等待前者的完成。这实际上是两个线程的同步。•如果一个执行线程没有被任何线程对象代表,那么这个
执行线程是分离的(detached)。用thread类模板的默认构造函数创建的新线程就是这样的。另外,新线程可以主动与创建者线程分离(detach)。第十章多线程1410.3C++的多线程库10.3.1
头文件<thread>10.3.1.1thread类voidf(){/*做一件相对耗时的事情*/}intmain(){std::threadt(f);return0;}这段代码将导致异常的发生。原因是:主线程(main)先于子线程t结束,而f
具有joinable属性。解决办法是在创建线程t后,调用:t.join();这意味着主线程main要等待t执行完。另个一个办法是在创建线程t后,调用:t.detach();这意味子线程t可能没执行完就被主线程杀死。第十章多线程1510.3C++的多线程库10.3.
1头文件<thread>10.3.1.2名字空间this_thread•yield()此函数的功能是为其它线程提供被调度的机会。如果一个线程可能会占据CPU资源太久,那么可以在其中调用此函数,给其它线程执行的机会
。•sleep_until(abs_time)调用此函数模板的线程会被阻塞,直到超时。超时时限是个绝对时间,由参数abs_time指定。•sleep_for(rel_time)调用此函数模板的线程会被阻塞,直到超时。超时时限是个相对时间,由参数rel_time提供。第十
章多线程1610.3C++的多线程库10.3.2头文件<mutex>10.3.2.1mutex类•有用的成员:•lock()由线程发起,申请进行加互斥锁操作。一旦申请线程加锁成功,那么它将成为这个锁的拥有者;否则,申请线程会被阻塞。在拥有者解锁之前,其它申请用
同一个mutex对象加互斥锁的线程都会被阻塞。解锁后,被阻塞的线程会得到调度的机会。•unlock()•锁的拥有者发起此解锁操作。此后,原线程不再是该锁的拥有者。原则上讲,锁的拥有者必须在适当时候解锁,否则会造成死锁。mutexm;voidprin
t(inti){m.lock();cout<<i<<'';m.unlock();}mutexm;voidprint(inti){lock_guard<mutex>lock(m);cout<<i<<'‘;}//lock_guard的析构函数自动解锁第十章多线
程1710.3C++的多线程库10.3.3头文件<condition_variable>•一个线程可能把自己阻塞,并等待一个事件的发生;事件发生后,该线程恢复执行。•完成这种功能需要定义在头文件<condition_variable>中的(多种)类实例对象的参与。头文件
中,最重要的类是condition_variable。第十章多线程1810.3C++的多线程库10.3.3头文件<condition_variable>1.voidwait(unique_lock<mutex>&lock)设有c
ondition_variable类的示例cv,当线程t调用cv.wait(lock)时:•wait自动调用lock.unlock(),并将线程阻塞在cv上。•如果线程处于非阻塞态,则调用lock.lock(),然后返回。•如果有解除阻塞信号到来,则wait将解除阻塞。简单地说
,就是如果没有接收到解除阻塞信号,那么wait将处于阻塞状态,不会返回;线程也因此处于等待状态。2.voidnotify_one()将阻塞线程中的某一个解除阻塞。3.voidnotify_all()将所有阻塞线程解除阻塞。第十章多线
程1910.3C++的多线程库10.3.3头文件<condition_variable>condition_variablecv;template<intid>voidg(){thread_localunique_lock<mutex>lk(m);print("thread",id,"isw
aiting...");cv.wait(lk);print("thread",id,"terminated");}threadt1(g<1>),t2(g<2>),t3(g<3>);t1.detach();t2.detach();t3.deta
ch();while(true){cin>>i;if(i==1)cv.notify_one();elseif(i==2)cv.notify_all();elsebreak;}三个线程与主线程分离,后面的while语句才有执行的机会。线程被阻塞线程被调度
pleasepressanumbertoterminatethread(s):1for1,2forall,3forexitthread2iswaiting...thread3iswaiting...thread1is
waiting...1thread2terminated2thread3terminatedthread1terminated3bye!第十章多线程2010.3C++的多线程库10.3.4头文件<future>•线程的执行往往是异步的。因此,如果一个线程依赖于另一个线程的结果,那么这两个线
程需要同步。•这样的同步操作可以使用定义在头文件<future>中的类模板future和promise来实现。第十章多线程2110.3C++的多线程库10.3.4头文件<future>•术语解释1.两个异步执行的
线程使用共享态(sharedstate)来进行结果通信。共享态包含了一些状态信息和(也许现在还没有产生的)结果。2.这些结果被封装在一个异步返回对象(asynchronousreturnobject)中。3.一个异步返回对象的等待函数(waitingfuncti
on)处于潜在的阻塞状态,等待共享状态已准备好(ready)。4.一个异步供应者(asynchronousprovider)对象提供共享状态中的结果。一旦异步供应者准备好,那么等待函数将立即获得结果。第十章多线程2210.3C++的多线程库10.3.4头文件<future>10.3.4.1f
uture和async•future类模板定义了一个异步返回对象。它的模板参数是结果的类型,其主要成员是get(),用于从共享态中读取结果。调用get()的线程将处于潜在的阻塞状态,等待共享态准备好。•future类模板一般和异步函数模板a
sync一起使用。async用于以异步方式(在一个潜在的新线程中)启动可调用对象,并提供一个封装在future对象里的、可调用对象产生的结果。//线程样板是一个lambdafuture<data>result_f=async(launch::async,[]()->da
ta{this_thread::sleep_for(milliseconds(1000));return{1,'A'};});//get导致调用它的线程被阻塞dataresult=result_f.get();第十章多线程231
0.3C++的多线程库10.3.4头文件<future>10.3.4.2promise类模板•Promise是一种异步程序设计规范,多种程序设计语言实现了对它的支持。•promise是一个异步供应者。它的模板参数是结果的类型,其成员set_value()为线程提供可获取的结果;另一个成员get_
future()为线程提供了获得future对象的接口,这个future对象的类型参数与promise的一样。此后,线程通过该future对象获取promise提供的结果。promise<data>pro;a
utoresult_f=pro.get_future();threadt([](promise<data>&p){this_thread::sleep_for(milliseconds(1500));p.set_value({3,'C'});},ref(pro));//lambda
不使用捕获,而是通过参数传递包围块中的对象t.detach();//get导致调用它的线程被阻塞autoresult=result_f.get();第十章多线程2410.3C++的多线程库10.3.4头文件<
future>10.3.4.3packaged_task•类模板packaged_task将可调用对象包装在一个任务(task)(是一种异步供应者)中,并且可以在这个任务启动时就设置任务将来返回的结果。线程通过调用这个模板的成员get_
future()以便获取任务的结果。packaged_task<task_t>tsk(task);autoresult_f=tsk.get_future();//callofmove()isveryimportant
!threadt(move(tsk),4,'D');t.detach();autoresult=result_f.get();第十章多线程2510.3C++的多线程库10.4多线程编程示例设有这样一个很小的餐厅:•有两位主厨(chef)、两位服务员(server)。当顾客点餐后:•两位主厨开
始做菜;做好一道菜后,一位服务员负责上菜。•所有菜做完后,餐厅关门。现在,不考虑顾客的参与,编写一个程序来模拟这个餐厅的主厨和服务员的工作情况。第十章多线程2610.3C++的多线程库10.4多线程编程示例10.4.1系统简要分析从案例描述
可以得出,这是一种典型的生产者-消费者模型的应用。在这种模型中,生产者负责产生数据,消费者来消费数据;二者的运作是并发的,要竞争数据的访问权。因此需要对二者进行互斥和共享操作。•2位厨师是生产者•2位服务员是消费者•竞
争的数据是订单和做好的菜第十章多线程2710.3C++的多线程库10.4多线程编程示例10.4.1系统简要分析餐厅的活动图餐厅的用例图第十章多线程2810.3C++的多线程库10.4多线程编程示例10.4.2系统设计对象设计•将主厨和服务员设计为两个类。•订单是一个list。这个l
ist被初始化为顾客的所有订单。•做好的菜也是一个list。开始的时候,这个list为空。主厨每做好一道菜,就将其添加到这个list中。第十章多线程2910.3C++的多线程库10.4多线程编程示例10.4.2系统设计流程设计•考虑到同时工作的实际情况,程序流程应
该是并发的。也就是说,需要设计主厨工作和服务员工作这两类线程。•为了线程在执行时能获得更多的信息,为主厨和服务员类重载()运算符,是它们成为可调用对象,然后用这些类的对象充当线程模板。第十章多线程3010.3C++的多线程库10.4多线程编程示例10.4.2系统设计数据竞
争处理•程序并发意味着数据竞争。这里,订单和做好的菜是两个主要的被竞争的数据。•对于主厨们,需要一个互斥量来保证同一时间只有一位主厨读取订单;需要另一个互斥量来保证同一时间只有一位主厨添加做好的菜。此外,每添加一道菜,需要向服务员们发出信号。•对于服务员们,需要一个条件变量来等待菜做好的信号
;此外,需要一个互斥量来保证同一时间只有一位服务员上菜(并从做好的菜列表中移除)。•另外,程序会用到标准输出流向屏幕输出信息。因此,需要一个互斥量来保证同一时间只有一个线程能够占据标准输出流。