《多核与GPU编程:工具、方法及实践》---- 3.7 经典问题中的monitor

3.7 经典问题中的monitor

3.7.1 重新考虑生产者–消费者问题

生产者–消费者问题中缓冲区的管理通常是轻量级的。然而,为了完整性,需要考虑基于monitor的解决方案,并考虑前面介绍的不同的设计方法。

3.7.1.1 生产者–消费者:在monitor内部管理缓冲区

在这种情况下,monitor仅需要为生产者和消费者公开put和get函数。特别是将之与信号量进行比较时,代码清单3-20所展示的解决方案可以清楚地显示monitor架构的表达能力显得更为简洁。

生产者和消费者代码简化为最短(第67~72行以及第98~102行),但是缓冲区管理的所有复杂细节都包含在Monitor的put(第20~29行)和get(第32~43行)方法内部。代码清单3-20与代码清单3-10在生产者和消费者终止(亦即给定需要处理的资源的总数)上有许多相同的特征,模板类的使用也使得其可以支持各种特殊情况。

与代码清单3-10中的代码不同,这里的生产者和消费者并不了解共享缓冲区的内部工作机制,也不通过信号量进行互相通知。所有的通信都在Monitor类内部隐式实现。

Monitor类使用两种等待条件——fullempty,当队列满时(第23、24行)阻塞生产者线程,当队列空时(第35~36行)阻塞消费者线程。注意,Monitor类使用大量变量,而这些变量在代码清单3-10的线程间共享并用于避免竞争条件。现在在两类线程间的唯一共享变量是Monitor实例,并通过initClass方法使得两类线程获得这一共享变量。

3.7.1.2 生产者–消费者:在monitor外插入/抽取缓冲区

如果在共享缓冲区中添加或者移除资源需要花费一定的时间(例如需要拷贝对象而不仅是引用),则使用第二种设计方法可以有效提高性能。从monitor中获取和释放一个permit意味着run方法将会花费较之上一设计中更长的时间。

这种思路是,生产者和消费者将会使用一对函数,首先获得缓冲区位置的互斥访问,然后释放给monitor继续使用。

代码清单3-21中的解决方案的关键之处如下。

Monitor类提供以下两对方法。

Pdroducer线程使用canPut和donePutting方法

Consumer线程使用canGet和doneGetting方法

这些方法的主体主要包括代码清单3-20中putget方法的两部分(平分)。
canPut函数和canGet函数返回指向缓冲区位置的指针,该位置可以用来存储资源的索引。通过inout指针而不是count计数器的加一,实际上防止返回的位置被Monitor进一步修改。

canPutcanGet函数返回之后,ProducerConsumer线程可以充分利用时间来存储或者提取一个资源。Monitor此时可以继续服务于其他线程。

一旦调用donePuttingdoneGetting方法,计数器加一或者减一,并且一个等待的Producer或者Consumer线程通过empty或者full等待条件通知。

3.7.2 重新考虑读者–写者问题

基于monitor的解决方案也可以移植到读者–写者问题。应用monitor而不是信号量,为一个线程或者其他类型的一个线程分配优先级变得十分简单。问题的特征使得必须使用第二种设计方法,亦即每个线程都将获得访问资源的允许,并且在完成任务后释放这一允许。

线程无须考虑优先级,或者不用考虑其他位于关键区的线程的存在性。这一功能嵌入在monitor的方法中。

在下一节将要介绍的三种解决方案中,ReaderWriter线程执行固定数目的操作,如下所示:

下几节中将要分析monitor实现的具体细节。

3.7.2.1 优先考虑读者的解决方案

为了赋予读者线程优先级,必须维护等待的读者线程readerWaiting,并且在readerWaiting大于零时阻止一个写者进入关键区。

代码清单3-22所展示的解决方案的重要之处如下。

monitor维护两个等待条件:用于唤醒等待写者的wq和用于唤醒等待读者的rq。

在读者线程内部关键区中维护一个计数器,在第29行加一,而一旦读者线程离开时将在第44行减一。如果计数器为0,信号将会被发送给所有等待的写者线程(第46行)。

如果有读者线程位于其关键区内,则写者线程会阻塞,否则写者线程将进入关键区(第35行)。

最后一部分实现优先级到读者线程的转移,并在finishedWriting方法中管理等待条件队列:只在没有等待的读者线程时才唤醒一个写者(第53~56行)。

3.7.2.2 优先考虑写者的解决方案

为了赋予写者线程优先级,必须维护等待写者队列writersWaiting,并且当writersWaiting
大于零时,阻止读者线程进入其关键区。

代码清单3-22和代码清单3-23中的两个类是近似等价的。其区别仅在于控制关键区的入口和退出关键区的队列管理上。在前一解决方案中,如果有写者线程等待在其关键区入口,第22行将强制读者阻塞。将优先级转交给写者线程在代码清单3-23中的第52~55行实现,一个写者离开其关键区时将会唤醒一个等待的写者线程(前提是有一个写者进程的优先级高于几个读者进程)。但是如果没有等待的写者线程,所有的读者线程都将被唤醒(第55行)。

3.7.2.3 公平方案

为读者–写者问题设计一个公平的解决方案是一项具有挑战性的任务。尽管这还未在之前的章节中考虑过,为了满足在关键区入口的请求,从一个等待条件队列中释放线程的顺序是实现一个先入先出顺序的关键。

Qt文档中有关wakeOnewakeAll的方法写道:“线程被唤醒的顺序依赖于操作系统的具体调度策略,不能预先控制或者预测。”

值得注意的是,这并不是Qt实现的错误。一旦一组线程重新就绪,其执行的顺序就是操作系统的职责,不能直接控制。一个等价的问题会对任意monitor实现带来困难。

Qt文档提供了一个关于如何处理这一问题的启示:“如果想唤醒指定线程,典型的解决方案是使用一个不同的等待条件,并使不同的线程等待不同的条件。”

代码清单3-24展示的解决方案恰好展示了这一特征:被强制阻塞的线程等待不同的条件,允许显式精确控制唤醒哪一个线程以及按照何种顺序唤醒。一个固定的等待条件数组按照循环队列模式分配和管理(in和out索引以及一个用于记录使用数目的counter)。附加一个布尔型数组(writeflag)允许区分何种类型的线程被阻塞在每种等待条件上。如果所有的等待条件都被使用,线程将会被强制移出并加入到一个通用的等待条件(quefull)。这其实是与完全公平的解决方案唯一的不同之处(由于线程离开队列时不按先入先出队列),但这是一个微妙的折中。

代码清单3-24中代码的其他一些关键点如下。

所有请求进入关键区的线程首先检查等待条件队列的状态,以及在其关键区执行的线程的种类。

只要没有写者线程在队列中并且等待条件队列为空,读者线程就允许执行canRead(counter==0)。如果后一种情况为假(第30行),这意味着在服务顺序中至少有一个写者位于这一读者之前。

如果等待条件队列为空并且没有其他写者或者读者线程位于关键区内,写者线程就允许执行canWrite(第48行)。

如果第30行和第48行的条件为真,使用c数组中的一个元素阻塞线程,writeflag中的对应元素被设置/重置,以表示有一个读者/写者线程被阻塞。

当调用finishWriting,并且等待条件队列为非空(counter>0)时,位于队列头的线程类型(out指针指向)被检测(第80行)。如果第一个元素是读者,队列中的所有读者线程或者直到遇到一个写者线程之前的读者线程被唤醒(第82~86行)。否则,第一个写者线程被唤醒(第91~93行)。

以这种细粒度方法管理尝试进入关键区的线程能力,可能导致多种可能性,它们超过这种简单的读者–写者场景。例如,可以用任意目标函数判断将要执行哪一个线程(例如,基于优先级、资金因素等)。一个恰当的例子是满足客户请求的多线程DBMS系统。这可能通过客户的级别、请求的优先级或者任意其他标准集进行排序。

时间: 2024-09-23 10:19:47

《多核与GPU编程:工具、方法及实践》---- 3.7 经典问题中的monitor的相关文章

《多核与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章 多核和并行程序设计 2.1 引言

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

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

3.6 monitor 信号量提供一个十分方便的机制,如果合适地应用,可以通过多线程的实现来获得最大并行性.然而,这只是"如果".由于它提供十分细粒度且横跨多个线程的程序逻辑并发控制机制,因此高效地使用信号量机制是十分困难的.在面向对象编程时代,信号量已经变得不如从前流行. 幸运的是,存在另一个称为monitor的机制.monitor是一个对象或者模块,可以供多个线程进行安全访问.为了完成这一功能,某个时刻只允许一个线程执行一个monitor的任意一个公有的方法.为了阻塞一个线程或者通

《多核与GPU编程:工具、方法及实践》----3.8 动态线程管理与静态线程管理

3.8 动态线程管理与静态线程管理 3.2.3.1节介绍过,Qt管理一组就绪的线程池,不需要操作系统来分配和初始化新线程实体.尽管创建线程的开销较之创建进程的开销要小几个量级,但它仍然是较为耗时的,特别是当线程需要在运行时动态生成时.一个经典的粒子是监听请求和分配线程进行服务的并发Web或者数据库服务器.在这种情况下线程可以从一个空闲线程库中选取并重用,而不是为每一个请求创建一个新的线程.QThreadPool类提供的功能正是这种线程库. 本节将要介绍如何利用QThreadPool,以及如何创建

《多核与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.这里父线程和子线程的关系也是如