《多核与GPU编程:工具、方法及实践》----3.6 monitor

3.6 monitor

信号量提供一个十分方便的机制,如果合适地应用,可以通过多线程的实现来获得最大并行性。然而,这只是“如果”。由于它提供十分细粒度且横跨多个线程的程序逻辑并发控制机制,因此高效地使用信号量机制是十分困难的。在面向对象编程时代,信号量已经变得不如从前流行。

幸运的是,存在另一个称为monitor的机制。monitor是一个对象或者模块,可以供多个线程进行安全访问。为了完成这一功能,某个时刻只允许一个线程执行一个monitor的任意一个公有的方法。为了阻塞一个线程或者通知一个线程继续执行(建立关键区的两个基础功能),monitor将提供一个称为条件变量的机制。

一个条件变量通常只允许在monitor内部访问,它包含一个队列并支持两个操作。

wait操作:如果一个线程在一个条件变量上执行等待操作,它将会被阻塞并被放置在条件变量队列中。

signal操作:如果线程在一个条件变量上执行通知操作,将会有一个相应队列中的线程被释放(即变为可用)。如果队列为空,则信号被忽略。
等待和通知操作类似信号量的功能,但是存在一些不同。在条件变量上的等待操作导致线程的无条件阻塞,而信号量上的等待操作依赖其取值。另外,条件变量信号可以被忽略,而信号量的释放操作总会将其加一。
条件变量总是与一个条件相关联,亦即指出线程应该阻塞还是继续执行的一组规则。这也是monitor较之信号量的最大优势之一。由于任何复杂的条件可以不借助一系列相互交织的信号量获取轻松处理,就像我们在读者–写者问题中看到的一样。
作为一个简单的示例,考虑一个执行银行账户的交易处理的monitor类。withdrawdeposit方法的伪码如下。

为了保证只有一个线程在AccountMonitor实例中运行,两个方法的入口都被一个互斥量锁定。在出口处被释放(第19行)互斥量,或者当一个线程将要被阻塞时(例如发现没有足够的资金可供取出),并且被放置在条件变量的队列中(第12行)时,释放互斥量。当资金被存入账户后,阻塞的线程被唤醒。
当线程被唤醒时发现存入的资金仍然不足时怎么办?如果有多个被阻塞线程,并且可能至少有一个线程的取款条件可以满足,但是并不一定是被唤醒的线程,此时该如何
处理?
上面描述的程序逻辑显然没有覆盖这种情况。接下来的讨论将会解答这一问题。
1.线程立即运行。由于一个monitor不能存在两个执行其方法的活动线程,因此这将自动导致发出唤醒信号的线程挂起。一旦被唤醒的线程推出monitor,线程就将执行。这种方法与Hoare monitor规范一致,Hoare是在19世纪70年代最早提出monitor概念的科学家。
2.唤醒线程将处于等待状态,直到信号发出线程退出monitor。由于这一延迟,直到线程获得monitor的控制权,其等待的条件才可能被修改。这就需要一次重新检查,将控制等待的if语句变为一个while语句。这种方法遵循Lampson-Redell monitor规范,它较之Hoare monitor具有一些优势,包括具有超时等待能力以及唤醒所有等待条件变量的线程的能力。这些实现都靠等待的线程重新运行时对条件的再次检查。
为了区别行为的不同之处,信号的通知操作称为notify,通知条件变量的所有等待线程的操作称为notify all
monitor实现的主要部分(包括Qt和Java中的实现)都遵循Lampson-Redell规范,因为额外的功能和内部更简单实现同时存在。使用这种monitor,可以完成银行账户monitor示例,从而解决上述两个问题。我们将在研究Qt中的monitor机制后重写代码。
Qt提供QWaitCondition类来支持条件变量,它包含以下方法。
wait,强制线程阻塞。为了避免程序员分别加锁和解锁控制monitor入口的互斥量操作,互斥量的引用将会被传递给自动执行这些操作的方法。
wakeOne,唤醒一个线程。
wakeAll:唤醒所有阻塞线程。所有线程都将在montior内部顺序执行。
Qt提供更为方便的类,它简化了控制入口的互斥量管理:QMutexLocker。这个类的实例应该在每个monitor方法的最开始创建,它负责对控制入口的互斥量加锁,这也是为何向其构造函数传递它的一个引用。这一看上去冗余的操作的优势在于monitor方法可以在多个程序位置终止,而不需要显式的互斥量解锁操作。当QMutexLocker类的析构函数被调用时(方法终止时),互斥量将被解锁,减少开销并且消除潜在的编程错误。
代码清单3-18展示了重写新的银行账户monitor类的方法。

可以简单区分monitor方法的三个部分,其中两个是可选的:
1.入口(可选):检测条件是否满足线程继续执行。
2.中间:操作monitor状态,并且执行需要互斥的任意操作。
3.出口(可选):通知其他线程可以进入monitor或者继续执行。
请读者在代码清单3-18中找出这三个部分的代码。
由于在关键位置包含对应的程序逻辑,因此Monitor提供的互斥机制极大地简化了多线程应用的设计,并且使得其更易于理解和修改。
已经证明Monitor和信号量是等价的,所有使用信号量的程序可以转变为使用monitor的程序,反之亦然。解决方案的复杂性是问题的独特特性。
根据关键区的位置基于monitor的解决方案可以使用两种可能的模式。一种选择是将关键区放置于monitor内部,另一种选择是使用monitor获取和释放关键区的入口。每种方法都有各自的优势和劣势,这将在下面几节进行介绍。

3.6.1 设计方法1:monitor内部的关键区

将一个关键区放入monitor内部是一种完美的方案,由于每个时刻只允许一个线程执行。但是这种策略只在关键区相对来说较短时有效,否则会很低效,并且在极端情况时可能会将一个多线程程序的效果变为一个串行程序。上面展示的这个银行账户问题实例遵循这一设计方式,由于depositwithdraw方法的执行时间是可以忽略的,因此这是可行的。
另一个可以应用这一设计方式的场景是使用monitor进行控制台或者文件输出的顺序化。

这个例子说明了这种方法的致命缺点:如果输出到不同文件时又该如何处理?如果使用以下代码是否能正常工作?

答案依赖于writeOut函数的调用频率以及使用的输出流的数目。尽管在原理上这种方法是次优的。一种更好的解决方案是为每一个输出流分配一个不同的monitor对象或者按照下一节介绍的方式设计一个monitor

3.6.2 设计方法2:monitor控制关键区的入口

在这种方法中,monitor作为一个互斥量使用,亦即包含一对方法,一个用来允许进入关键区(获得允许),另一个用来通知离开关键区(释放允许)。尽管这看上去像是一种模仿互斥量的加锁解锁序列的过于复杂方案,但实际上monitor可以支持较之互斥量关键区入口更细粒度的控制。
例如,考虑“吸烟者问题”:它包含三个吸烟者和一个代理者线程。每个吸烟者连续制作香烟并吸完。为了制作香烟,吸烟者需要三种原材料:烟草、纸和火柴。这三个吸烟者中的每一个都只有一种原材料并且数量是无限的,亦即一个只拥有纸,一个只拥有烟草,而最后一个只拥有火柴。代理者线程拥有这三种原材料且数量是无限的,并且只随机选择两种类型的原材料放到桌子上。随后通知这两种资源的可用性并阻塞。缺少这两种原材料的吸烟者可以从桌子上获取并制作香烟,随后需要一个时间随机的窗口用于制作香烟。一旦完成香烟制作,吸烟者就通知代理者循环这一过程。
需要强调的是,这个问题定义较之典型的问题定义更为复杂。在典型的定义中,代理者只通知需要的吸烟者开始制作香烟。而在这里需要吸烟者自己判断是否能继续执行。
代码清单3-19展示了这一问题的解决方案。

这一解决方案包含三个类:一个Somker类,用其所需要的原材料类型来进行标识(第3~5行枚举了原材料);一个Agent类,包含一个预定义的迭代次数(在构造函数中定义);一个Monitor类,用于允许其他两个类进行通信。这一解决方案的关键之处如下。
Monitor提供了以下两组公用方法。
吸烟者线程中用到的canSomke和finishedSomking对,对应get permit和helease permit类型的函数。
Agent线程中用到newIngredient和finishSim方法。
Agent调用Monitor提供的newIngredient方法,传递可供使用的原材料类型。Monitor唤醒所有的等待线程(第31行)然后强制Agent线程阻塞,直到某个吸烟者完成执行(第32行)。
Agent线程负责程序终止。在迭代次数指定之后,调用monitor的finishSim方法。这将翻转monitor内部的一个布尔类型的标志(exitf,第52行),并唤醒所有在条件变量w上等待的线程。
MonitorcanSomke方法返回值非零时,每个Smoker线程执行一个负责终止的循环。后一种方法返回exitf标志的值。
这个程序的一个运行示例如下:

时间: 2024-10-08 17:01:08

《多核与GPU编程:工具、方法及实践》----3.6 monitor的相关文章

《多核与GPU编程:工具、方法及实践》----第2章 多核和并行程序设计 2.1 引言

第2章 多核和并行程序设计 本章目标 学习设计并行程序的PCAM方法. 使用任务图和数据依赖图来识别可以并行执行的计算部分. 学习将问题的解法分解为可并发执行部分的流行的分解模式. 学习编写并行软件的主要程序结构模式,如主/从和fork/join. 理解分解模式的性能特点,如流水线. 学习如何结合分解模式和合适的程序结构模式. 2.1 引言 即使是对于经验丰富的专业程序员,向多核编程的过渡也并不简单.多核和并行编程往往会打破语句按严格顺序执行的串行程序的传统风格.当许多事情在同一时间发生时,正如

《多核与GPU编程:工具、方法及实践》----导读

目 录[第1章 概述 1.1 多核计算机时代 ](https://yq.aliyun.com/articles/90097)1.2 并行计算机的分类[1.3 现代计算机概览 1.3.1 Cell BE处理器 1.3.2 NVIDIA Kepler 1.3.3 AMD APU 1.3.4 从多核到众核:Tilera TILE-Gx8072和Intel Xeon Phi ](https://yq.aliyun.com/articles/90111)1.4 性能指标[1.5 并行程序性能的预测与测量

《多核与GPU编程:工具、方法及实践》----2.3 分解模式

2.3 分解模式 设计过程最困难同时也最关键的部分无疑是分解过程,即确定可以并发执行的计算.虽然任务图法是最常用的,但开发者无法从中获取以往的经验,这时就需要模式.Mattson等人[33]列出了若干分解模式(在他们的书中表示为"algorithm structure design space patterns"), 该参考文献包含了工作负载被分解并最终分配到并行或多核平台各个节点上的基本方法.图2-4显示了能得到6个模式之一的决策树. 上一节提到了两类分解,即功能分解和域分解,现在又

《多核与GPU编程:工具、方法及实践》----2.4 程序结构模式

2.4 程序结构模式 模式不仅可以帮助选择合适的工作负载分解方法,还可用于程序的开发,这正是程序结构模式的目标.接下来的一节将讨论和分析几个最著名的模式. 并行程序结构模式可以分为两大类. 全局并行局部串行(Globally Parallel Locally Sequential,GPLS):GPLS表示应用程序可以并发执行多个任务,每个任务串行执行.这类模式包括: 单程序多数据 多程序多数据 主/从 map-reduce 全局串行局部并行(GSLP):GSLP表示应用程序串行执行,当需要时,一

《多核与GPU编程:工具、方法及实践》----1.5 并行程序性能的预测与测量

1.5 并行程序性能的预测与测量 构建并行程序要比串行程序更具挑战性.并行程序程序员需要解决诸如共享资源访问.负载均衡(即,将计算负载分配到所有计算资源上来最小化执行时间)以及程序终止(即,以协调方式暂停程序)等相关问题. 编写并行程序应该首先确定并行能否提升程序性能,加速问题的解决.并行程序的开发成本决定了程序员不可能简单地实现多个并行版本,通过测试找出最佳和最差版本,来评估项目的可行性.虽然对于最简单的问题,这个方法可能是可行的.但是即使能够这样,如果能够确定一个先验的最佳开发路径,并按照这

《多核与GPU编程:工具、方法及实践》----第1章 概 述 1.1 多核计算机时代

第1章 概 述 本章目标: 了解计算机(计算机体系架构)设计的发展趋势以及该趋势如何影响软件开发. 学习基于Flynn分类的计算机分类方法. 学习评估多核/并行程序性能即加速比和效率的必备工具. 学习测量和报告程序性能的正确实验方法. 学习Amdahl和Gustafson-Barsis定律,并使用这两个定律预测并行程序性能. 1.1 多核计算机时代 在过去的40年中,数字计算机已经成为技术和科学发展的基石.遵循20世纪70年代摩尔(Gordon E. Moore)发现的摩尔定律,计算机的信息处理

《多核与GPU编程:工具、方法及实践》----3.2 线程

3.2 线程 3.2.1 线程的定义 线程可以被认为是轻量级进程.更精确的定义是线程是一个执行路径,亦即一个指令序列,可以被操作系统作为整体单元进行管理调度.一个进程中可以有多个线程. 线程可以减轻原有生成进程机制中的开销,仅需要拷贝基本的数据,即运行栈.由于线程包括被调用函数的动态框架(或动态记录),因此运行栈不能被多个线程共享.共享栈意味着控制权可能返回一个与调用线程不同的位置. 当一个进程的主线程(初始线程)生成一个新线程时,最终的内存布局十分类似于图3-3.这里父线程和子线程的关系也是如

《多核与GPU编程:工具、方法及实践》----1.4 性能指标

1.4 性能指标 发展多核硬件和开发多核软件的目标是获取更高性能,例如更短的执行时间.更大规模的问题和更大的数据集等.很明显,这需要一个客观的标准或者准则来评估这些努力的有效性. 最起码,一个并行程序的执行时间应该要比其对应的串行程序的执行时间短(然而,并不是所有情况都这样).执行时间的缩短通常表述为加速比,可用如下公式表达: (1-1) 其中,对于解决同一个问题实例,tseq是串行程序的执行时间,tpar是并行程序的执行 时间. tseq和tpar都是时间,但它们并不总客观.二者会受到如下因素

《多核与GPU编程:工具、方法及实践》----第3章 共享内存编程:线程 3.1 引言

第3章 共享内存编程:线程 本章目标: 学习线程的定义以及创建方法. 学习完成特定任务的初始化线程方法. 学习多种终止多线程程序的技术. 理解多线程访问共享数据过程中的主要问题,例如竞争和死锁. 学习信号量和监视器的定义和使用方法. 熟悉经典同步问题及其解决方法. 学习运行时动态管理线程. 学习多线程程序的调试技术. 3.1 引言 从20世纪60年代麻省理工学院引入兼容分时系统(CTSS)以来,多个程序并发执行的现象已经变得较为常见.操作系统通过中断当前正在执行的程序并将CPU的控制权交由另一个