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

本节书摘来自华章出版社《多核与GPU编程:工具、方法及实践》一书中的第3章,第3.7节, 作 者 Multicore and GPU Programming: An Integrated Approach[阿联酋]杰拉西莫斯·巴拉斯(Gerassimos Barlas) 著,张云泉 贾海鹏 李士刚 袁良 等译, 更多章节内容可以访问云栖社区“华章计算机”公众号查看。

3.7 经典问题中的monitor

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

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

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

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


8c68c1ec9297e288bec9b4a3372a15046018ac1f


50660d12a94fe17f819e3c5235451ace3a5ae48d


b42c5a091d5285430841775c4ff609a08fcf8312

生产者和消费者代码简化为最短(第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继续使用。


7ec89f2c892f6c1c7746c76f5bcabf1370342a4c


edcb1ca0a092223483cd471279ad9c9eb974023a

代码清单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线程执行固定数目的操作,如下所示:


5b409b147fc47551989500541c0e5de039e13c45

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

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

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


a821d7d9472c44a09d4117d624dd062e810a69b5


c22b50c9328123d927650ef0946722145db6e57d

代码清单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行)。


91d9dbecb7e9e2d02acef5202e302c00ccd6864b


79757852a22bdb52a7da6602d3811104992070b5
3.7.2.3 公平方案

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

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

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

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

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


996b6d97aea150527c81b83b3a5095bf4e8c29ca


bfd6863da81e3456f05bf09b43351f7752a5f117


96e968d654ab30e0ad8cbe4ceeae65112ef81c36

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

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

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

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

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

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值