case study of control

from: http://www.chinesemooc.org/kvideo.php?do=course_progress&kvideoid=4859&classesid=2048

下面我们给出几个软件系统开发中的实际这个处理

并发系统和这个顺序系统决策的这个例子

就是说现在呢是一个,第例一呢是一个教务管理系统,这个教务员呢进行

登记学生的一些学籍,查询一些学生的一些信息统计学生考试成绩的这么一个小程序。

这个本身呢如果说是这样的一个系统的话可以完全按照这个并发系统,顺序系统来考虑,就是不需要考虑并发系统的问题

但是呢,有些情况下呢,这个业务要求这个教务员呢需要同时录入多个学生的成绩

或者说在一个学生,录入一个学生的成绩的时候呢这个,在一个界面上录入一个学生

的成绩,在这个做了工作做了一半的时候,可以录入另外一个学生成绩这个回过头来这个录完之后再录这个第一个学生的成绩

是吧,这个它是在工作中的一些便利条件,因为它在录这个学生成绩的时候,可能有些情况下找不到了,或者说这个需要这个

先录第二个学生的成绩,但要保留当前状态怎么办?

如果这种情况下呢也不要,也不需要进行额外的一些开发,就是需要

这个它同时借助于这个操作系统,如果这个操作系统能够是一个多进程的系统的话,是吧

它可以在这个我的这个操作系统中打开另外

这个一个进程,这样的话呢就可以实现这个录入

在执行的时候不会产生错误,这就是进程的一个功劳,是吧,没有进程的话不可能实现这种东西。

但是呢,需要注意的是它最后存盘的时候呢这个是不是能够把这两个学生的这个成绩同时保留下来,这个恐怕

不一定行,这个需要一方面依赖于你这个文件格式,另一方面可能还需要一个文件同步。

这个另外一个情况下呢就是它现在升级了,这个系统,它采用这个

一种文件服务器的方式,由于这个教务系统中啊不同的教务老师

是吧,一个教务老师录一门成绩,一门课程的成绩,另外一个可以同时录入另外一个

这个课程的这个成绩,是吧,这样就是一种本身现实世界中他们就是那么做的,要求实现为

IT 系统的时候也这样做,这样提高效率。

这时候这个用前面那样那种方案就不行了,为什么?就是因为它们没法实现这个

在一台机子上两人同时录,即使能够实现的话那也效率也是没有什么这个提高。

为此呢,需要开发一种这个分布式的应用系统这时候需要至少有两台以上的这个客户端,或者

PC 然后呢还有一个文件服务器,这样的话呢可以来 实现这种方式。

并且呢在这个开发的时候呢我们可以实现一种文件服务器的

加锁和解锁机制,是吧,这就是说这个如果加上锁的话,然后他们之间就不需要,不能允许

第二个这个教务老师呢来这个进行,访问这个文件但如果没有加锁的文件两个人可以同时访问,这样的话呢这个可以实现

一个人录一个课程成绩,形成一个文件,另外一个形成另外一个,二者不会冲突为什么?但是在开发的时候呢,也不需要去

考虑什么并发的问题,你按顺序系统开发就可以了,最后也是借助于操作系统

或者说一种网络操作系统的一些便利条件来实现这个问题,并且在这个如果说需要或者说一些文件共享的一些机制。

还有一种情况呢就是进一步的升级这个不是一个简单的一个文本,现在是

整个学院的教务,甚至整个学校的一个教务,它分为很多的一些院,院里边有很多一些教务

老师,然后同时录入,这时候用文件系统就不行了,用文件系统这时候你把这个文件拷到本地- 然后再拷回去之后效率太低了

为了解决这个问题,我们采用了CS 结构,这是也体现了我们这个分布式系统的一种发展趋势,是吧,这时候这个

用CS 方式这个来实现,CS 可以实现一种大规模的并发,是吧,这个并且呢

它不需要像文件服务器那样,是吧,把文件这个整个文件复制到本地PC

机上,然后进行编辑,编辑完之后存回去,效率太低了。

它是一种这个CS 这个查询,把它们这些所有的成绩都放到关系数据库里边去

放关系数据库里边就通过这个SQL 进行远程查询

这个查询过来之后的结果,只把这个查询结果筛选出的查询结果返回到我的

PC 机,这样的话呢 这个可以实现这个,便于这个网络传输的时候,这个数据呀,可以得到了很大的一个

数据量得到了很大的一个缩小,这样便于效率。

另外呢在这个关系数据库系统,面向网络的关系数据库管理系统中啊

它加入了很多的一些处理一些数据一致性的一些机制

可以实现多个用户同时访问数据,然后呢同时修改一个数据库中的一个

数据,或者说同时数据这个,一个修改一个表中的不同

的字段,不同的这个记录,使得最后的结果呢是保留这个,把二者这个同时修改的时候呢,把这个

所有这个修改全部保留下来,这样来实现这些东西都是后台它这个关系数据库系统通过一些

非常复杂的一些机制来实现,这个我们就不多说了。

但是对用户来说,对编程人员,终端编程人员来说呢这些东西都是对他们透明的,所以他在开发的时候呢,实际上就是完全按照一个顺序系统来开-

发就可以了开发完之后部署,是吧,在每一个PC,每一个客户端上

给它装上一个我们的,部署到我们这个每一个PC 上,这样的话呢

部署完之后,借助于这种网络的操作系统和这个面向多用户的这种数据库管理系统,这样很好的实现一个并发

端程序员来说,编程的时候呢他完全按照一种顺序系统来开发就可以了,这就是另外一个例子

这个以上这几个例子都不需要什么呢?都不需要一些这个在设计的时候要考虑到

我们这个多进程多线程的问题,是吧,还有一种系统呢就是下面这个规模更加庞大,是吧

就是现在这个学院,现在已经这个,或者说学校这个异地办学,在很多时候

学院很多,这个办学条件,这个在异地进行办学,这时候呢需要,如果说按照前面那种

CS 结构就需要大量的 维护工作,是吧,当我这个系统发生升级,业务逻辑发生修改的时候

由于你这个学院在异地,这样的话呢需要这个有专人,维护人员这个带着光盘到

当前的这个本地进行安装升级,这样呢会带来很大的麻烦,是吧

即使是这个异地办学呢,由于这个学院非常多,教务人员非常多,这个维护

也是非常大的,需要你这个在不同的学院之间来回穿梭进行维护。

为了这个提高这种维护性,实际上就产生了一种新的一种开发的一种这个分布式的一种模式,就是BS 浏览器服务器的这种方式。

浏览器服务器实际上就是这个中间呢有一个应用服务器,然后有一个数据库服务器

然后它们之间借助internet 和 intranet 进行这个与这个 PC 机进行联系。

PC 机上实际上就是一个浏览器 它没有任何逻辑,就是个浏览器,所有的

这个界面逻辑呀,和这个业务逻辑呀,全部封装在什么呢?

部署到这个管理系统的这个应用服务器中

应用服务器借助于这种非常高效的一种这个网络,这个与这个数据库管理

服务器进行这个通信,来实现这个业务逻辑这样有什么好处呢?就是不需要这个维护人员进行这个

实际到这个实际的这个客户端进行维护,客户端什么也没有装,只要装上IE 就可以

就可以了,IE 又是系统自带的,所以这个不需要任何维护 这个 IE

只要你这个给它一个网址,然后呢就可以这个登录访问,一般情况下。

而所有的维护工作全部都在一个地方,都在这个中央机房,或者这个计算中心来进行维护,是吧,只要这个当我们的业务

教学业务发生变化的时候,只需要升级这个应用服务器和数据库

管理服务器中的一些数据和一些程序就可以了,这样使得

这个我们的这个维护工作非常简化,这就是BS

的这个优势这是以上这些情况下呢都不需要这个我们在控制驱动

部分进行设计,也就是说在进行设计的时候我们只需要设计人机交互界面问题域部分的设计,数据管理部分就可以了。

控制与驱动部分全部交给部署的时候,由这个底层的一些软件,支撑软件来完成的

这个当然现在呢这个又进入了一个新的时代,BS 结构呢

这个又进一步这个演化为一种云架端这种方式,是吧,就是实际上就是不需要这个

PC 机了,这个前端肯定是在手机上 手持的一些设备,一些非常简单的这个平板上就可以这个实现

这个在后台呢也不需要开发人员或者说

一些维护人员对于一些这个服务集群进行维护,全部放到这个云上,然后很多情况下都是自动维护的,是吧

本身服务器的一些升级维护,这些东西全部都不需要了云服务提供商,这个甚至云服务提供商本身也不会自动

手工的完成这个东西,全部都是机器自动化的完成,这个提供非常这个可靠的这个

72 小时不间断的一种服务,这样的话呢这个交给一些非常

专业的这一种这个云服务提供上要比这个单位内部的一个

网络管理员或者系统管理员来维护起来这个更加有效,系统更加这个这个

健壮,不会出现一些滥用机制这种情况,但是这只是云计算的一个方面,当然它还有其他方面。

这是这个还有一个问题就是说,以上四个例子中呢全部都不需要我们进行控制系统

服务设计,那么什么时候需要进行控制系统服务设计呢,这时候需要

就是对于某些系统不得不考虑这个问题,比如说现在考虑一个

遥感,卫星遥感系统接收和显示系统,这个系统需要这个一个卫星天线

接收来自于这个遥感卫星传递的一些

遥感数据,这个遥感数据是一种非常大数据,非常密集数据密集型的一种信息

它一秒钟要传递非常这个高清的一些这个照片,是吧,传递这个

每隔这个几百个毫秒就传递下来一幅,传递下来之后,马上实时性地显示

哎这时候我们考虑一下是不是,不用这个多进程线程控制系统的方式就可以实现

那么这时候呢数据下来之后呢首先呢有一个工作就是要把这个数据进行什么呢

进行这个解包,还有一些有一些这个把这个帧头帧尾去掉之后,把它里边的这个iii代码读解出来看看

这个首先要接收这个数据,从这个

天线中的缓冲区中把这个数据读解出来,把它们接收进来,这需要

一个从天线的缓冲区中传递到我的当前的一个这个服务器中的缓冲区中,这是一个

一部分工作,而另外一部分工作就是数据处理,是吧,把这些

一帧数据然后把它们进行这个拼接,进行这个帧头帧尾的这个筛除,然后把这些

中间的这个iii代码到底给它读解出各种各样的信息,是一个什么图像元素,然后进行拼接 这个操作。

就是显示,解出来之后显示在内存中知道是什么一个

一帧数据了,然后就是要把它显示到屏幕上,这是进行一个图像处理

但是如果说假如现在这几个任务我们用一个顺序的方式来完成,我

就是一个进程,然后呢这个进程首先呢把这个,从这个天线借助于一个数据通信,是吧。

从天线那边呢把一帧数据取到我这个

本机工作机上来,然后这一帧数据取来之后,然后呢我们就进行处理,进行转换产生

在内存中产生一个,根据这个信息然后产生一个图像然后产生完之后再把这个翻过来,表面上这是可以了

但是你没有考虑到什么呢?就是个时间的一个问题,是吧。

现在要时实地显示这个你光解,比如说光这个接收,可能就是整个这个数据量非常大

光接收可能需要这个一秒钟,然后呢又把这个或者几百个毫秒,然后呢

这个如果把这个数据处理一下又产生几百毫秒然后再一显示又是几百毫秒,这样的话呢这个显示的时候呢

可能是一帧一帧的成幻灯片了,本来我们这个遥感信息要求是一种实时性的一种,这就相当于这个

一种这个视频信息,现在变成幻灯片了。

那你更重要的是什么呢它这个数据量,卫星下来的这个数据呢是非常

流速非常快的,你在这儿一帧一帧地解,最后导致什么呢这个你的缓冲区会溢出,到你的这个接收过来的这个

要么是在这个天线那边的数据产生缓冲区域,出现或者是在你的这个当前的

服务器中这个没有处理的这些数据会产生溢出,这样的话呢是非常有问题的设计。

为了解决这个问题呢你必须要进行这个什么呢?多个这个进程,这个或者多个控制流的设计

那么第一种方案呢就是说我们

用进程的方式比较实现,因为进程它这个可以借助于这个CPU

这个的一个编制条件,借助于操作系统编制条件可以很好地实现

有些情况下不需要编程,程序这个执行变成一个进程实际上是由这个操作系统自动完成的。

但是又遇到一个什么问题呢,我们这个输入是一个进程处理是个进程,然后呢显示一个进程,这样的话呢有个什么问题就是

进程之间我们知道,内部它可以封装的非常好,但是呢

它进程之间的通信呢是一个非常大的问题,这个是要么是

借助于共享文件或数据库来进行通信,要么是借助于这个进程间通信,用特殊的编程技术

不管是哪一种方式,尤其是文件的方式又涉及到外层,这个效率更低。

是吧这个进程间通信涉及到这个共享内容这个效率高一些,但是呢

无非就是把这个进程内部的一个内存

一部分内存块然后复制到一个共享的内存上,然后呢另外一个进程上面取

也会带来一定的这个时间上的一些损失,这些损失呢

总体来说不足以支持我们这个非常流畅的一种遥感数据的一个产生。

因此这种方式不不可行的,为解决这个问题呢必须要用线程的方式

线程一个好处是什么呢,就说是这个尽管它们这个有这个分配了

CPU 资源,但是没有给它们分配一些独立的一些内存

资源,这样的话呢它们可以在一个进程内给它变成三个线程,然后它们可以共享数据。

这样的话有个什么好处,这个接收下来的数据由输入线程呢

进行接收,然后呢由这个这个线程处理进行这个处理然后由这个显示线程进行显示,它们可以共享这个数据,这样的话呢

就会使得这个效率非常高,是吧只要避免用这个进程间这种这个

处理加注一些这个处理资源的一些能力,这样的话呢就可以非常实现了

这个很好地实现它们高效之间的一种,非常高效的一种

数据传递,这个并且编程的时候呢也非常简单,对吧,就是在这个共享一个内存

是吧,这个共享一个数据结构,它们都可以在这里边取就可以了。

这样的话呢,你并且在更重要的是,在这个实现的时候呢可以非常好的提供了一种效率

这样的话呢,这个非常方便地完成我们这种这个性能上的要求

所以在这种情况下呢,我们用一个进程给它分多个线程的这种实现是最优的一种方案

这是就体现线程之间这个共享数据是非常容易的,也带来非常高效率。

更实际的例子我们就是说接收下来的数据并不是一个简单的一个实时显示,还

需要存储,还需要进行拼接,还需要进行数据分析,这时候呢这是更复杂了,有些情况下可以变为一个进程,因为它们

之间没有非常这个高的实时性,可能是一个这个由其他的一个进程来完成更好一些,因为你这个进程来说呢

编程的时候,或者是说这个由不同的节点来完成的时候,这时候用进程是非常好的一种实现方式

所以在这种情况下呢,更复杂的一种情况了。

就是它实际上呢是多进程多线程的一种方式

前端是由一个单线程,单进程,多线程的程序负责接收和显示,而后端呢由这个

多个进程这个应用程序来进行这个分析拼接和处理,这是一种最复杂的一种情况

下面我们看,借助几个例子,来看一下这个

前面讲的这套方法,如何用这种方法进行控制驱动部分的设计第一个例子呢是多线程产生动画的一个例子

这个实际上我们这前面讲到那个完整的游戏的例子的时候也提到,只不过这个

实际上不是一个很实用的一个例子,但是主要是通过例子我们可以来

理解一下这个我们前面我们讲的那些线程,特别是线程

这种控制流实现,是如何实现的,以及在Java这种 编程机制下,如何应用这个Guarded,就是说

不通过,两个线程之间用这种自发的方式来实现这种协调

不通过第三者协调,专门的协调线程来进行协调,这种方式

这是一个什么意思呢?就是我们现在是有这么一个需求,就是说

在一个板子上,或者说一个屏幕上首先呢有一个线程要写,有两个线程在写

然后写一个线程写一个红的,一个线程写一个蓝的方块,然后呢写完之后呢两个线程释放掉资源之后

由第三个线程来把它从内存的数据结构中把它复制到屏幕上,这样就显示出来了

然后接下来它办完之后呢,这个另外,它就休眠了,这个搬运线程休眠,然后呢另外一个

这个线程呢,另外两个线程又出来之后进行写第二帧数据

也就是说,把这个原来的这个擦掉,然后再计算一个偏移量之后再计算,这样的话就会显示这就会这个显示出来。

那么这个系统的这个控制流的设计呢,简单的来说就比较简单了这个系统,所以我们这个设计的时候呢

首先呢是一个主线程,这个是一个就相当于我们当前的一个

这个线程不是协调者啊,它本身就,主要是用来这个,就相当于,本身这个

这个主窗体,它呢聚合了thread player

1 player 2 player 3,三个线程 它实际上在需要的时候呢将这三个线程产生出来就完了,它没有提供任何的这个

三个线程之间这个协调的问题,另外呢还有两个什么呢一个这个内存中的一个main,还有一个什么呢,一个

设备,是吧,一个就是说这个,它们这个数据结构是一样的这个第三个线程在搬运的时候,只需要将main这个线程

这个有main这块这个 数据结构,把它复制到这个graphics上就可以了,这样就 显示到屏幕上。

但是光有这个,如何实现这个三个线程之间的协调问题?是吧?这三个线程之间属于这个

需要进行协调这一类,类型。

不是说这三个线程之间这个跑起来之后各自不管个人了,个人运行个人的就可以了。

它们之间需要一个密切的协调这个协调呢,一方面是数据的协调,一方面是这个步骤的协调,是吧?必须是player 1

和player 2,player 1 和player 2 之间谁先谁后没关系 这个它先写它先写都没有关系

但是对于任何一帧数据来说,player 1 和player 2之间,谁先写谁后写没关

但是呢,写完之后必须呢后面这个线程马上是player 3

把它搬运回去,搬运到屏幕上,这个线程,它搬运完之后呢,接着player 1 和player 2 之间呢,player

1 和player 2 接着再过来再写,这个顺序是不能,必须是严格遵守的

不能说是player 1 写完之后player 2 写,然后呢写

完之后呢它再写一块,然后这个写了两块,写了两个循环之后,第三个线程才来搬,那样就使得这个线

这个整个过程就不够流畅,为解决这个问题,实际上这个没有必要是吧?实际上我们主要是为了什么,非要三个线程,两个线程就可以了

为什么搞这两,搞三个线程,我们主要是为了让它变得复杂一点,然后呢来这个

体现我们这种协调,协调的这个机制。

我们用什么协调方式呢?我们就是用了第三种,第二种协调方式,用Guarded这种方式,是吧?用它们三个线程之间-

自发这种这个协调,借助一种这个Guarded,标记为Guarded的这种

方法,这个方法呢,实际上是有一个flag类 来这个表示的,这个flag类有什么作用呢?就是

说任何的一个player在进行运行的时候呢,它会 调用这里边的一些Guarded的操作,它在调用的时候呢

这里面有个机制就是说,它在调用的时候呢,其它的线程必须得保证其它线程没有调用这里边的所有的操作,如果调用的话

那么它就会自动的会进入休眠状态,这是它Guarded的一个 一个语义。

这是借助于Java里边同步的那种 一种机制来很好的对应过来,这样的话进行实验,如果

你这个编译器不支持这种方式你就没法用了,你就得用其它的方式

下面我们具体看一下这个通过顺序图来描述这个线程之间如何产生的以及如何交互的

这个很简单的,就是这个主线程啊,产生完三个线程就不管了,然后它们三个

自生自灭,自我调节,主线程没有任何的协调作用这个产生完之后第一个线程来说,第一个线程呢是这个player

1,它负责写那个红色的一个小的方块 它写之前呢,首先是什么时候它开始这个

进行这个激活状态,那就是由其它线程notify它,通知它

唤醒它,然后它就起来,唤醒之后它所做的第一件工作就是要调用flag

1 的这个askf1 askf1 的时候呢,实际上这个f1

是个特殊的,askf1 是个Guarded的一个方法,是吧?它在调用的时候不允许其它方法再调用其它这个类里-

边其它的方法所以这就便于它们之间协调它调用一下,如果f1=1,那么就表示着什么呢?

当前的这个板子,实际上是被那个正在,不适合你现在正

在写,因为现在正在是第三个线程正在搬运的时候,搬运的时候你写是不允许的这样的话呢它就让它在休眠,让这个线程休眠

一旦f1=0 的话那么实际上就是什么呢?

就是可以写了,这时候f1 已经搬完了,这时候你可以再写了,你这时候写的时候呢

这个这些过程呢我们后面再说啊,这个写的过程由后面来进行描述,写完之后要释放资源。

释放掉f1,那就是置f1=1,同时呢

notify这两个线程进行这个,把这两个线程叫醒,然后让它这两个线程起来跟它一- 块iii

但是这个notify 1 和 2 之后呢,这儿我们有一个什么比较巧妙的地方就是说

它在向这两个线程发送notify叫醒唤醒这个消息 之后,它本身没有直接进行休眠

而是什么呢,而是它又回到了这个循环里边再次请求f1,由于这时候f1

已经被自己置为1 了,所以它 这时候才进入休眠状态,而由另外两个线程呢再进行协调

需要注意的是,另外就是说这个notify是一个同步消息,这里边为什么同步消息,- 就是因为

它在notify的时候这个player 1 不做任何事情,它就等待这个消息

那么player 2 已经处在休眠状态,就在那儿死等着

这个通知消息来唤醒它,所以这是个同步消息。

好,下面这是第二个player 2,player 2 跟player

1 很像,也是被叫醒之后它要调用一个 这个Guarded方法标注的同步消息,叫askf2,f2

这个f2 的时候呢是这个正好是它如果说 f2=1

然后它就等待,否则的话它就开始做自己的业务,最后也是一样。

它跟这个刚才那个这个区别就在于,第一个是这个在这个叫醒的时候呢首先要查询一下f1

是否等于1 第二个是查询f2=1,那么第三个线程是什么呢?它们与那个

它的工作关系和那两个是互斥的,那两个本身来说呢谁先做谁后做是没关系的这时候呢这个player

3 出的搬运线程,实际上是叫醒之后,被叫醒之后

呢它是要查询一下,是否f1=0 或者说是f2=0 只要有一个等于

0 那就表示那个正在写,它就不能搬,否则的话呢

那么就是说什么呢,f1=1 并且f2=1 这两个都已经写完并且要退出来的时候

它才能进行搬运这个操作,搬运完之后同时释放掉,也是释放掉这个相当于把f1 f2

都置为1,然后通知另外两个线程起来 这个这样的话呢这个它们就产生一个非常好的一个协调作用,也就是说

尽管它们是三个线程,按理说应该是这个各自执行跟各自没有关系,但是借助一种同步机制

它们可以很好的实现,第一个线程写第二个线程写,然后呢第三个线程

搬,搬完之后呢第一个再写,第二个再写,当然也可能是第二个写,第一个再

写,这个都没关系,但是呢必须是这两个都写完之后第三个才能搬是这么一种情况。

这个在这个真正工作的时候是如何工作呢?player 1 是首先呢将原来的那块

自己写的那块那个小的一个正方形给它擦掉,涂黑,然后计算一个偏移量之后在新的位置- 上再重新

写一个画一个红色的矩形,而player 2 呢是正好也是一样,只不过它

画一个蓝色的矩形,player 3 呢实际上就是将这个image 中的一个参数

这个把这个image复制到设备上,这个屏幕设备上去。

这是一个简单状态图,体现player 1 的一个状态,这个状态图呢是产生,new产生之后,然后这个进行就绪

就绪的时候,如果f1=1,然后这种 这个事件,这是一个条件事件,激发它进入休眠状态

如果被其它的一个player 2 和player 3 其它线程叫醒的时候它就进入就绪

如果f1 进入等于0 的话那么它进入运行状态,那就是它进行工作状态

工作完之后呢,然后这个运行完执行完一个release

f1 之后呢又进入这个就绪状态,这是它的一个简单状态图来描述

下面我们就通过演示的方式,我们来看一下用java语言

如何来实现这个控制驱动不同的方案,以及这个运行效果。

好,下面呢我们看一下

这个多线程动画部分的这个实现,我们这是实现

上一节我们这个建立的这个模型的这么一段这个

Java 代码,它的这个结构呢,跟我们上节课

讲的这个类图的是完全相同的。

首先呢这是那个Flag 类 Flag

类呢,主要是用来进行这个信号量,这个准入和准出,这里呢实现了有两个

synchronized 这个同步,加上同步关键字的这个

两个这些方法,这些方法呢实际上共三个线程。

三个线程分别是这是那个最主要的那个

动画类,然后呢这个动画类呢在开始的时候呢,它产生在

初始化的时候呢,它这个进行了一些

这个初始化的工作,主要是包括把这个缓冲区

初始化起来,然后产生一个新的Flag 类。

然后再,它在main 函数里面呢实际上是 就是产生这三个线程:这三个线程呢分别是

player 1, player 2, player 3。

player 1 和 player 2 它们两个主要就是,player

1 是画这个 一个相当于一个红色的一个

小矩形,而player 2 呢是画一个

蓝色的这个小矩形。

我们看到呢,在run 这个方法里面,实际上呢它 是画在这个一个缓冲区里面去的

这个,边界呢在画之前呢,它首先要进行这个请求,请求一个

临界资源,当这个别人没有占有这个临界资源的时候呢

它才能这个进行往下,下面进行

这个把原来的这个颜色铺黑,然后再计算变量之后呢

再做一个,偏移之后呢

在新的一个位置上画一个蓝色,这个这是这个整个这个过程。

这个player 3 呢实际上呢它的 run 方法就是

这个在请求到两个当player

1 和 player 2 都已经释放掉资源之后呢,它再把它们搬运上来

下面我们看一下这个运行的情况

我们发现呢

啊,对的

如果我们按照我们那个,刚才那个

模型中所这个表达的那个意思来进行这个

实现的话呢,我们发现在这个player 在

ask f1 的时候呢,实际上呢是 f1 等于

1 但是呢,如果我们那个

就是请求请求那个f1

请求f1 的时候呢, f1 等于 1 如果 f1 等于 1 的话呢,就表示这个

缓冲区呢正在被复制到这个屏幕上,所以呢它要这个休息。

然后呢如果说是player 2 呢,它是请求 f2 等于 1

如果f2 等于 1 ,那就是意味着这个它所使用的这个

缓冲区呢,仍然是在由这个player

3 这个线程呢正在使用。

如果这个但是呢如果player 1 等于 0,或者说 player 2

等于0 的时候呢 实际上这两个线程呢并没有这个约束,实际上它们两个可以并行的

或者说并发的同时化而这个player

3 ,当这两个等于 1 的时候呢 它们就可以进行这个实现,我们看一下这个执行的效果

实际上呢我们可以看到

在执行的过程中呢,会有一些混乱的这个情况,这实际上这是为什么呢?

就是我们刚才所说的这个问题,尽管呢我们可以看到在这个执行的

过程中,它们的这个顺序是player 1,player 2,player 3 player

1,player 2,player 3,player 1,2,3,但是呢 213,或者是

213 就是说,只有当 player 1 和 player

2 不,这个做完一次之后呢,player 3 在中间插入

但是player 1 和 player 2 到底谁先谁后呢,并没有一个 一个这个明确的一个定义。

但是这个时候呢,实际上是那就意味着刚才为什么会出现这个问题呢?那就是因为

player 1 和 player 2 实际上呢这个没有一个信号量,或者说是一个

centralized 的方法,然后呢这个来同步它们的 共享资源。

因为这个它们两个在同时使用缓冲区的时候,实际上缓冲区本身也是一个共享资源

正是因为我们当前的这个逻辑,它没有这个对这个

缓冲区,这个共享资源,这个临界资源呢进行

只允许一次只有一个线程访问进去,所以就刚出现了刚才我们出现了那样的混乱的一个情况。

我们再看一下这个我们发现呢在这个过程中呢,实际上呢是这个有很多这个

出现乱的一个线条,这是我们和我们这个编程的时候呢,实际上那个

最后的这个意图呢是,这个不太一样的。

就是说,它实际上为什么会出现这个呢?就是由于它们两个线程呢同时使用相同的

缓冲区,缓冲区临界资源,如果这两个线程

同时画的话那就会造成一定的逻辑混乱,这些逻辑混乱是我们有些时候呢无法预知的。

你看像这种情况呢很有意思,是这个,这个红色的这个矩形呢,它这个

有一些这个蓝色的边框,而这个蓝色的这个矩形呢,它有一些

这个红色的边框,所以如何解决这个问题呢?

那实际上呢,我们可以再进一步的改进,我们下一步改进的时候呢我们可以看到

我们让它们在任何情况下呢不会有两个线程同时使用相同的缓冲区。

这样的话呢其实也很简单,就是我们这改,稍微改一下这个

这个在这个同步同步方法里面的我们改一下它们的逻辑

就是说当第一个线程,要请求这个临界资源的时候呢,它叫f1

不等于1 如果 f1 等于 1

的话那么的就是意味着这个,现在呢就它可以

这个获取这个临界资源,然后呢做一些工作。

而这个这个如果它这个不等于

1 的话 那就意味着被其他的两个线程所占用,所以呢这样的话就

这个,就被那个进入睡眠状态。

而player 2 呢也是一样,它 是当它这个是,当

player 1 那个释放临界资源的时候呢,它实际上是实际上 f1 等于

2,那就意味着 这个让 player

2,这个 线程呢进去访问临界资源,也就说它释放掉资源之后呢

下面非常明确的规定,player 1 释放掉资源之后呢,只有

player 2 然后才能够 接下来使用, player 3 不能使用。

而player 2 接下来呢,如果说是这个等于 2 的时候,然后它就进入,进入之后呢

由这个在释放资源的时候呢,它让f1 等于 3。

这样的话呢最后的f player 3,它在这个进入的时候的这个条件是

f3 等于 3 的时候,它才能够这个进行搬运。

这样的话呢,就是使得这个这三个线程呢,在任何时候访问临界资源的时候啊

就是这个缓冲区的时候,只有一个线程在访问。

这样的话呢,就就是一个非常有序的一个效果,我们看一下它的这个执行效率,执行效果是个什么样的

我们看见在修改之后呢这已经是非常这个平滑、

非常这个好看的一个画面,这它们两个这个

这个由于它是这个双缓冲技术,所以呢它的这个展示界面呢非常漂亮、非常稳定,并且呢这个也没有

出现上面我们出现这个前一个例子出现那种,非常混乱的那种这种线条。

并且我们看一下这个这个线程的执行顺序呢是非常严格的,player 1,player

2,player 3 player 1,player 2,player 3,player 1,player

2,player 3 ,所以它们之间呢 正好由于这种不同的限制,所以导致它们不同的这种访问风格。

还有一种呢就是我们这个如果说不加限制,让这三个线程

分别抢的话,那就是一种非常糟糕的现象。

这个这个代码中呢,实际上我们没有任何的这个

centralized 这种同步的一些方法,是在这三个线程呢进入

这种临界资源的时候,比如说这个缓冲区的时候实际上是无序的状态,谁抢到,这

有时候三个线程可以同时访问这个资源,我们看一下它这个效果是什么样的

[无声] 我们可以看到这个效果呢就更加混乱

甚至还不如第一种情况,就是因为它这个由于这三个线程呢是无序的一种情况啊。

所以呢,有些时候,不仅有存在这种这个线条

这是两个工作player 1 和 player 2 在争夺资源,另外呢

这个player 3 ,由于它们无序,所有 player 3 呢实际上

它在搬运线程的时候呢由于它比较快,所以它可能这个抢到的比较的多。

所以呢,它可以这个,我们可以看一下这个这个序列,是吧。

实际上呢这个player 3 呢有些时候抢到十次之后,十几次之后呢

这个player 1 和 player 2 才能够抢到一次。

这样的话呢,就会造成这个画面呢非常不流畅,就是因为它这个一直在搬,一直在这个复制

复制这个缓冲区,但是player 1 和 player 2

没有抢到缓冲区,你就没有这个产生新的一帧数据,实际上都是一个原有的那一帧数据再复制过来

所以,偶尔抢到一次,然后呢这个实际上呢这个,就显得画面非常这个不流畅,像动画片一样。

这是大概的一个就是说非常小型的,这种多线程

的这个基于Java 多线程的这个程序设计。

当然呢我们是首先建立一个UML,这个类图和顺序图或者是状态图之后呢,然后呢

在考虑比较清楚的情况下呢才进行的

下面是第二个例子就是电梯调度器这个电梯调度器的这个实际上这个状态图呢,我们在状态图

部分的这个,那部分里边给大家这个讲过,下面我们主要看一下如何实现,实现就是说这个

当前的这个状态图,这个状态图这个实现本身呢也是一个多进程的,这个多线程的一个程序

那么我们现在考虑是用C++ 来实现,跟第一个例子不太 一样。

这个我们可以通过这个来考查不同的这个编程语言如何实现的

控制驱动部分设计,因为它正好是个多线程的方式,所以需要进行控制驱动部分的设计。

那我们这个C++ 呢也是用到的是那个 MFC ,啊,这个

Visual C++ 啊,这种方式这个进行这个实现的

下,借助Windows 消息,这种 MFC 这种特有的这种 Windows

消息这种方式来实现所以,在这种情况下我们看如何实现。

在这个真正的讲这个设计方案之前呢,我们先看一下这个,简单介绍一下在

MFC 中 Visual C++ 中啊有两种线程。

这两种线程分别称之为工作线程和用户界面线程,是一种非常这个

经常见到这一种这个流程模式,是吧

在云计算这种编程方式下,这个也是有两种,在我们开发云服务的时候

也有两种类似的这个控制流,一种叫工作working

rule,啊,一种叫这个 Web rule, Web rule就是个界面,是吧,负责与这个

客户端打交道,那么working rule就是负责这个真正工作。

这那两个是进程,这两个是线程,啊所以这个进程和线程之间一个统一的地方就是它们之间都有一个消息队列,一方面

都是这个封装了一些这个可计算的一些资源,另一方面它有一个消息队列不管是进程线程都有消息队列。

所以呢这个工作我们这里工作线程和用户线程这个区别就在于什么呢?

这个工作者线程呢,它只负责干活,只负责分配一个CPU

资源,它就在这个基础上干活,给它布置个任务,它就在那不断地在那循环着

这个做这个工作,它没有这个消息循环队列其实呢,真正线程都是有的

只不过这个消息循环队列呢被封装的看不见了对用户来说看不到这个消息队列,而对于用户

界面线程呢,它有消息队列,它也可以非常好地这个实现Windows

消息放到这个消息队列里边去,实现这个消息循环它是用这种模式。

所以这个工作的线程主要负责一句话简单来说就是负责干活,负责计算冗长的线程

而这个用户界面线程负责通讯,然后呢这个负责这个

线程和线程之间的这种这个消息传递

这个响应一些用户的信息,但是在启动这个线程的时候呢实际上是没有区别的。

注意在这个C++ 中这个实现这个封装线程和在 Java

中封装线程这个编程机制是不太一样,一般来说,Java

中呢是通过继承一个Thread 类就可以实现 在这个 C++

中呢,它实际上是通过把这个组合,在这个产生线程的时候,将一个线程

组合到我当前这个类中来实现这个线程,主动类的方式,这个。

这个是大家的一个方案,控制驱动的方案。

这个要实现这个线程呢,是吧要实现这个电梯类,首先呢必须有一个界面,这个界面呢

就,就模拟这个电梯啊,在这个每一个楼层上的一个运动的这么一个UI

,是吧,这个我们可以认为是一个定义一个CElevator Dlg

这么一类,它也封装了一个线程,它是个主界面,它也封装了一个线程。

然后呢,这里边的组合了多个这个button

,这个一些按钮,是吧,这些按钮表示了哪个楼层,用户按了个上,按了个下,或者说在一个轿厢内,用户按的哪一层。

另外呢,它还有一个什么呢?有一个这个工作线程,这个工作线程

呢,实际上就是负责与之外的用户界面这个线程,这是个用户界面线程,它们负责

与这个用户界面之间通信,也就是用户那边点了一个按钮,比如说在,在第三层点了一个上

这个是通过一个,转换,一点这个按钮转换为一个Windows 消息 通过

host message 传递给当前的这个 CElevatorControl 呃,

这个Thread ,这是一个我们刚才讲的这个用户界面线程 它有一个消息队列,它用,它比较进行通信,比较方便地

进行通信,就跟人一样,有些人喜欢这个做技术工作,有的人负责与人沟通这就是负责与其他线程之间沟通的这么一个线程

然后呢,它又组合了两个这个真正的工作线程,一个叫决策线程,一个叫这个执行线程,这个分别对应了我们那个呃

这个,我们这个以前讲到这个状态图中的两个正交区,是吧,然后呢还有一个呢是一个非

μTags 这个用来进行一个信号量,这个跟前面是一样的,进行这个线程之间的同步

首先呢,这是一个线程的产生,在线程产生的时候呢,跟Java

类似,但是呢这个使用的方法呢不太一样,是吧,就是第一个线程

如果说要启动,在这个用户界面,电梯的用户界面这个对话框里边点击启动电梯,是吧,这时候呢需要有一个这个

Windows 消息,然后 Windows 消息 实际上是一个异步消息,然后放到

Windows 这个,放到当前线程的这个消息队列里边去,通过消息循环呢

在适当的一个时候呢,查找到这个消息,然后呢根据这个消息的映射,由这个消息映射的是

OnStartButton 这个 方法,然后呢再调用这个方法,啊。

这是Windows 这是这个 MFC 中这个消息的一种这个激发方式

它用异步的方式来进行实现,这种方式实际上在C# 里也有所体现 只不过 C# 不叫这个 Windows 消息了,它封装为事件。

接下来呢这个它在这张,StartButton 的时候实际上就产生了一个什么呢? 产生了一个

CElevator,刚才那个 CEle, 控制线程,也就是用户界面线程

它是如果产生了调用一个API,是吧,这个 AfxBeginThread

,这个这种方式来具体我们就不用说了,实际上是,是一种这个

产生这个线程实际上是将一个线程啊,这个作为一个参数

这个传递给它,这样的话这个类就产生了一个线程。

产生这个线程之后这个接下来呢这个线程就开始运动,就开始这个处在激活状态

这个线程呢接下来又产生两个工作线程,注意产生两个工作线程的时候呢这个调用的而这个

API 函数呢,跟这个前边是 很像,很像,但是有所不用,就是说它只是一个操作名是相同的

但是这个传递的参数不太一样。

这个也就是这个Windows API ,或者说这个 MSA

中产生线程的时候呢稍微比那个Java 中有所区别,但是不管怎么说 大概是一样的。

产生两个线程,这三个线程就开始工作这个实际上是四个线程,第一个线程呢是这个状态图之外的这个用户界面

其实真正实现状态图的电梯的调度状态图是这三个类然后呢,这个电梯控制器,或者说这个

电梯的用户界面和这个真正的这个状态图中的这个控制类是如何进行通信的呢?也是通过什么呢?通过

Windows 消息,它每点击一个按钮实际上它就向它发 送了一个,它把这个按钮转换成一个

Windows 消息,自定义的 Windows 消息,然后把它 通过消息队列的方式,放到它的消息队列里边进行传送。

然后这个消息实际上与一个OnElevatorMessage

进行了一个映射,这样的话呢每来了这一个消息以后,Elevator

这个方法进行,进行这个响应,由此呢就是说,不管这个用户在哪个楼层,或者说

轿厢里边点击哪个按钮,实际上呢都是这个都由Elevator

这个消息进行响应,这是这个当前这个就说这个用户界面和这个我们这个状态图中的这个控制线程进行通信的一种

实现机制,借助于MFC 来实现。

注意这里用的是PostTreadMessage API 函数,是吧,这个这是一个

Post 这个 这是一个消息作为一个参数,然后这个,这个是这个大概一个过程 然后这个

CElevator 这个控制线程呢,如果说是需要这个

有些比如说到达哪个楼层啊,或者说这个其它的这个这个消息向这个用户界面那个

dialog 进行这个反馈的话,也是借助于这个 post message

进行这个通信,这个我们就不用多说了下面呢我们看一下这个方案一,就是如何实现这个

决策者和执行者之间的同步,因为我们前面提到控制这个状态图啊,是吧

它实际上这个是涉及到两个正交区,每个正交区都分别实现为一个工作线程

然后整个外边那个线程,外边的那个这个大的那个状态如果响应事件的话,完全是由那个控制线程是吧

CElevator ExecutorControl 这个线程来负责,呃,这个响应外部的一些事件

第一个方案呢就是它们之间呢就是通过Cmutex

这种信号量这种这个类来进行实现

也就是大概呢就是说,因为决策和这个执行啊实际上不是

完全的谁也不管谁的这种执行,必须在决策的时候必须得等到这个

执行的时候呢,它停到一个楼层上,某一个楼层的时候,它不能运行时候决策

容易出错,它必须停到某一个具体的楼层之后,它才可以决策

那这时候如何实现这种线程之间的这种这个状态同步,或者说这个线程之间的一些这个

执行的到哪些过程之间的同步呢?这就是一个信号量,这个信号量实际上就是刚才那个

Java 中的方法很类似,是吧,就是这个

决策者在当前这种情况下是执行这个情况,是吧。

在执行exflag 等于 true 的时候,它是要不断地进行循环 一旦这个不需要执行的时候,这个

exflag 等于 false 呢给它退出循环,这个呢就进入终止状态

然后它这个先这个发送一个消息,向这个Cmutex 进行锁,锁定这个

Cmutex 如果锁定之后然后就表示没有人进入这个临界区了,然后他就可以执行

自己的这套业务逻辑,就可以进行决策。

到到底是这个向上走还是向下走,是大方向它可以决策出来

决策完之后呢,然后再解锁,另外一个working

这个执行者呢,实际上也是锁定这个这个Cmutex。

如果它这个锁定住之后呢,它要进行这个执行,这样

是任何时间内只有一个线程进入这个临界区,那就是保证这两个线程实际上一种这个只能是交叉进行,不可能同时进行

这样的话保证它这个逻辑性的正确性,实际上更这个符合这个UML2

这种表示法这种顺序图可以用这种方式来进行描述,是吧。

这是一个临界区,我们用到临界区的概念就马上就体现出来了这个临界区在执行的时候,不允许被其他线程打断,一旦我锁住这个

Cmutex 之后 整个执行这三个步骤实际上呢全部不允许中间插入来自于其他这个

组合交互片段中的其他的一些动作,这样的话呢就可以更清晰地描述这个组合。

当然有个什么问题,就是说尽管它能够保证这两个

线程在到临界区的时候,它是这个不允许被其他线程打乱,但是这个

步骤,它不能够定义清楚,也就是说有可能是什么呢我这个决策者锁住这个屏、

锁住这个Cmutex 之后,执行完之后,解锁之后,它又跟他抢,又跟这个决策者抢这个临界区

有可能他没有抢过他,它又执行了一遍,这还可能造成逻辑上的混乱。

为解决这个问题,我们可以就是说这个用那个第二个这个状态图,我们取消一个决策线程那这样的话呢就可以很好的实现了,是吧。

这个决策线程这个是实际上变成了一个取消这个正交区

这个正交区呢就变成了一个,变成一个这个没有正交区的一个符合的一个状态图,这样的话呢

只有一个工作线程,executor 是一个

线程,而这个决策者呢是附属在这个决策executor

一个被动对象,这样的话这样它在任何时候,需要它决策的时候,调用这个决策者的功能- 就可以了

这样的话呢是一个最简单的情况,也是最容易实现的一种情况,当然呢这个也可以用另外一种方式,就是用这个

MFC 中 的 CEvent 这个这种这个类,用这个也变成条件

这是一个对事件的一个支持,这个CEvent 有什么功能呢,就是

它封装了一种对事件的支持,允许一个线程在某种情况的发生的时候,唤醒另外一个线程执行。

它有两种状态:一种是有信号状态,一种是无信号状态

这个我们通过例子来说一下,这个大概就是这种情况,也就是说当前一个executor 在这些这个的时候

已经进行决策之后,决策完之后,马上要进行要让那个另外的一个线程啊,另外这个工作决策线程来

进行这个工作了,它就不能再次再决策了,那这时候怎么办呢这时候它要向这个

CEvent 这个对象发送一个 setevent 这个事件

消息,然后呢它在收到这个时候呢,实际上这个决策者在这个地方实际上在等待这个事件,只有当它收到这个

CEvent 发来的这个事件之后,它才能往下执行 因此在这个点上,它在这儿实际上是在这儿等待,出来是一个等待

那什么时候这个执行往下执行呢?也就是它在收到了这个CEvent 的一个事件之后,它才能往下执行。

那什么时候收到呢?就是由这个setevent 在当前的这个时候,另外一个线程在处在

某个状态的时候,它发送setevent,然后触发了这个事件的发生,这时候它才能够执行

那么通过这种方式就可以实现这个这两个线程之间这个过程上的一个同步,就不仅是一个零件区域

的一些不允许同时访问的问题,而且还可以实现过程上同步

这个还有一种方案呢就是方案四,方案四跟这个其实跟Java

,我们定义的例子中那种Java 的实现方式完全一样,就是用

这个临界区和这个全局变量这种交互来实现它们之间不仅是能够

一个线程访问临界区资源,另外是实现这个

它们之间从宏观上一种这个线程跟线程之间这种步骤上的一种同步

这就是实现,用临界区加上一个什么全局变量,是吧它两个都是这个要去试图要锁定

那个资源,Cmutex 资源,但是呢如果它是这个

锁定之后,它发现这个tex 等于 1 的话,它就要解锁并且呢

这个的动作,然后呢这个进一步地再去这个

争夺资源,但是呢如果再争夺上了,然后它还是该解锁就还是让,最后呢这个

肯定会被那个强调,强调之后呢就执行,是吧这个也是当Cmutex 等于 0

的时候,它要解锁也要要这个让步资源,也就是说

它只有当Cmutex flag 等于 0 的时候,它才往下执行

然后它执行完的时候呢,实际上它要把这个,当它这个抢夺到这个资源的时候它已经把这个Cmutex 等于

1 了,然后下一步执行的时候呢,它就不会执行它,必须要中间插入到这个之后 把这个 Cmutex 这个

flag 设置为 0 的时候,它才能够执行 这样的话呢也可以实现这个两个线程之间这个很好的这个

这个执行的过程中的这个序列行为的一些同步

你可能感兴趣的:(case study of control)