BUAA_OO_第二单元总结

BUAA_OO_第二单元总结

本单元通过电梯的模拟来学习多线程的设计模式,通过三次迭代开发,从最初的单部单型号电梯,迭代成为多部多型号电梯,虽然在初次学习多线程的时候感觉十分痛苦,但是完成了三次作业之后还是挺有成就感的。


第一次作业

架构设计

第一次作业的架构如下:
image
本次作业中只涉及到了一部电梯,对应三种不同的输入模式。在我的架构中,每个电梯包含一个策略类,负责根据不同的模式按照不同的策略调节电梯的运行状态。策略类都继承自一个Stra接口,三种模式分别写了不同的策略。但是根据写完之后的总结,我认为其实不需要策略类也行,因为策略类其实干的事情相对较少,直接写在电梯内部未尝不可。电梯的调度算法一开始采用了处理最近请求的算法,但是发现moening模式下的效果十分糟糕,导致很多点超时,遂改成了Look算法,并且当当前方向上没有请求的时候会改变运行方向。
另外针对morning模式,night模式以及random模式都在策略类中做了小小的优化,比如morning模式在一楼一定会开门,等够6个人或者输入结束才会出发;night模式会运行到有请求的最高层才开门。
第一次作业的UML协作图如下:
image
第一次作业采取的协作模式是标准的单生产者单消费者模式,在主线程中创造生产者线程Reqhandle以及消费者线程Elevator和托盘类Dispatcher,生产者负责将新增加的请求放到托盘类的共享队列中;消费者负责将请求从托盘类的共享队列中取出并且执行请求。当输入结束的时候,由生产者线程首先将结束信号发送给托盘类,再由托盘类将结束信号发送给电梯类,当电梯执行完当前请求之后自动结束。

锁和同步块的设计

第一次作业架构设计比较清晰,只有一部单型号的电梯和一个输入线程共享一个请求队列。其中输入线程负责生产请求,而电梯线程负责消费请求。

根据生产者消费者的设计模式,只设计了一个锁,用于锁住共享队列,关于共享队列的读取操作,添加操作以及删除操作都设置了锁。同时,为了能够安全的结束线程,我还在存放共享队列的托盘类当中设置了一个hasinput 变量,当读取到输入的末尾时将其置为1并且通知电梯线程,关于hasinput 变量的读取和修改操作也加上了锁。需要注意的是,在本单元的设计中,我所有的锁都是直接加在方法或者类上的,因为我认为这样设计能够最大程度上简化关于线程安全的问题。

在同步块的选择中,主要参考的一点就是:需要使用到之前synchronized 的方法的地方就设计为同步块,比如在电梯线程中将乘客送入电梯的reqIn方法以及需要扫描共享队列来对电梯状态进行控制的nextstate方法和对是否在该层应该有乘客进入的needin判断方法,都被设计成了同步块结构。

在这一次作业中,锁与同步块中处理语句的关系就在于同步块中的语句需要唤醒在这个锁对应的等待队列或者需要等待这个锁的唤醒(比如在结束线程的时候),或者同步块中的语句需要获得或者操作锁对应的共享变量。

调度器的设计

我认为在电梯的设计中存在两类调度器,第一类是总的请求分配调度器;第二类是电梯内部的状态转换调度器。

对于本次作业,由于只有一部电梯,所以总的调度器基本不需要特别的设计,只需要将所有的请求都分配给该电梯就行。总调度器负责将输入线程和电梯线程相关联,将输入输送给电梯。

关于电梯内部的状态调度器,根本来说就是电梯运行的策略,主要控制的是电梯的运行状态,最开始采用的是处理最近的请求的策略,当电梯有空位的时候处理最近的进入请求,满了则处理最近的出电梯请求。但这样做会导致morning模式下较大的问题,之后改成了look算法。并且针对不同的模式做了不同的电梯内部调度优化。

自己程序的bug

在本次作业中,由于之前设计的比较仔细,所以并没有出现线程安全问题。但是由于之前第一版书写look算法的时候,由于设计了只允许同向的人进入,导致在电梯掉头的时候总是会出现奇怪的bug,之后为了图省事直接改成了处理最近请求的算法。结果在morning模式下,由于处理最近的请求,导致直接变成了一趟电梯上行和下行处理一个请求,morning模式基本上全部超时了。之后在bug修复时改成了look算法。

发现别人程序bug所采用的策略

在第一次作业中,由于测评机在周二晚上才搭出来,并且我写的部分好像锅了,所以没有使用测评机来找别人的bug,只通过肉眼观察找了一下别人的bug,主要发现的都是策略上的bug,和我一样的原因超时了。(可能和我在的组有关系)


第二次作业

架构设计

第二次作业的架构如下:
image
在这一次作业中,由于从单部电梯变成了多部电梯,并且在程序运行的过程中会动态添加电梯,所以只在主线程中创建了输入线程和托盘类,在输入线程中启动电梯类,并设置了一个静态变量用来存放当前的电梯。在总调度器的设计上没有做过多的约束,基本的思想是让电梯主动抢人,在某一层楼能上人就上。并且由于这一次涉及到了多部电梯同时输出,增加了一个线程安全的输出类Output,避免同时多部电梯输出造成时序混乱。在单部电梯的调度策略中做了一点优化,由于如果是盲目抢人的话可能会导致多部电梯抢同一个人,所以当电梯内部没有人的时候,电梯会取处理距离当前最近的请求,并且会将其标记导致别的电梯不会再去访问这个请求。

第二次作业的UML协作类图如下:
image
基本的协作模式和第一次没有区别,主要的差别在于这一次的电梯线程并不是由主线程来启动,而是由输入处理线程来启动。并且由于只采用了一个共享队列,在区分请求和电梯的从属关系时只需要将请求做不同的标记,这样可以避免很多线程安全问题。在处理请求的时候也是由电梯主动去获得请求,而不是采用调度器分配,这样能够使电梯一直处于运动的状态,提高了效率。当输入结束的时候,也是先将结束信号由输入处理线程转移到托盘类,再有托盘类将结束信号转义给电梯线程。

锁和同步块的设计

第二次作业涉及到了多部电梯,老师在课上讲了一种架构是设置两个队列,第一个是总的请求队列,第二个是电梯的等待队列,我认为这样的架构可以在总的请求队列中将不同的请求做不同的标记对应到不同的电梯的,可以减少我的架构的修改(其实是懒)。所以第二次还是只有一个总的请求队列和一个托盘类。架构上基本没有太大的改变。

关于锁的设计,设计了两个锁,基本上和第一单元没有区别,就是在对共享队列进行操作的方法以及对hasinput 变量进行操作的方法上加了锁,并且锁上的方法都在托盘类里面。除了托盘类的锁之外,在本次作业中对每一个请求增加了一个分配的电梯的属性,对这个属性的读取和修改也同样设计成了带上锁的方法。除此之外还增加了一个线程安全的输出方法output,这一个方法也加了锁。

关于同步块的选择这一次和之前稍有不同,由于之前的架构中设计了三种不同模式对应的策略类,这一次在策略类中对于针对状态转移又设计三个方法,分别是上升,下降以及原地不动时的状态转移。这三个方法也设置成了关于托盘类的同步块,其余的同步块基本上与第一次一样。

关于同步块和锁的关系,我认为仍然和第一次作业中的结论一样,将需要唤醒或者等待锁的语句块变成同步块,以及将需要对共享队列的每一项操作的语句块变成同步块。

调度器的设计

还是从总的请求分配调度器和电梯内部的状态转移调度器来分析本次作业中的调度器设计。关于总的请求调度器,在和同学的讨论中,本来打算每一层请求分配都设置一个惩罚函数,通过惩罚函数的大小来对请求进行分配,但是仔细思考之后,我认为惩罚函数与电梯直接抢人没有什么很大的区别,所以没有对总的请求分配器做过多的设计,而是充分发挥每个电梯的功能让电梯抢人。在电梯抢到某一个请求的时候,会将请求的对应电梯属性修改为对应电梯的id,如果其他电梯同时访问到了这个请求,只能够读取这个属性而不能修改这个属性,保证了一个请求只被一个电梯处理。在本次作业中,总调度器的设计仍然是将输入线程和输出线程相关联。

关于电梯内部的状态转移调度器,针对每种不同的模式设计了不同的状态转移调度器,比如morning模式会在一楼等够6个人或者请求结束才出发;night模式会先去有进入请求的最高楼层接人。以及在抢人的设计上进行了一点优化,在电梯为空的时候会先去抢距离当前位置最近的请求,总体来看效果不错。

自己的bug

本次作业在强测和互测中都没有出现bug,但是自己在提交之前测试的时候发现了一个bug。那就是由于notify出现的位置不对,导致在某些情况下电梯进入阻塞状态,无法及时唤醒处理后面的请求导致RTLE。经过思考,我认为所有对共享队列或者hasinput 变量修改的地方都应该设计一个notify。

发现别人程序bug所采用的策略

本次作业中已经拥有了一个可以跑起来的自动测评机。可以测RTLE,WA和RE。但是CTLE由于不是在linux系统下运行而没有实现,通过自动测试试图寻找别人的bug,但是没有找到。也可以看出来在本次作业中,需要重点关注的并不再是第一单元中的正确性,而是和线程安全相关的真实运行时间和CPU运行时间。


第三次作业

架构设计

第三次作业的架构如下:

image

由于本次作业有三种不同型号的电梯,并且每一种电梯所能够到达的楼层并不相同,所以本次作业新增了将请求分配给对应种类的电梯,同种类的电梯抢人;以及实现了换乘(最后发现不换乘的效果比换乘好)。在主类中创建了输入线程以及托盘类,在输入线程中根据输入将乘客请求添加到分配器,或者按照增加电梯请求增加电梯。,并且在托盘类中根据每个指令的出发楼层和到达楼层将不同的指令分配给不同类型的电梯,如果要换乘则先规定一个换乘的楼层。在托盘类中新增了一个重要的变量reqsum,表示所有的指令数目,由于在换乘过程中需要重新处理指令的共享队列,如果不增加这一个变量可能导致线程无法正常结束。在电梯线程中,根据不同的模式实现了三种不同的策略,主要是在上升模式的状态转移策略中有所不同。

第三次作业的UML协作图如下:

image

关于协作部分的设计,我采用了比较简单清晰的三个线程,第一个是主线程,负责启动输入线程和创建总的请求分配器;在输入线程中启动电梯线程,并且将请求添加到总的分配器中,再在分配器中分配请求。电梯负责执行自己的请求。而当出现结束信号的时候,首先由输入线程将结束信号发送给分配器,再由分配器将结束信号发送给相应的电梯。

有关功能设计,实现了固定换乘点的换乘,固定了3楼作为ABC三类电梯的换乘点,15楼作为AB类电梯的换乘点以及19楼作为ABC电梯的换乘点,在请求输入的时候就静态为请求分配了换乘策略。有关性能设计,我认为本次作业中由于换乘的等待时间是不可预测的,需要按照最坏的情况来分析(也就是一个电梯周期),所以为了提高性能,基本上是能不换乘就不换乘,主要的换乘是AC和BC类电梯的换乘。

关于可扩展性的分析,我认为本次作业的架构可拓展性在总调度方面并不好,因为之前在设计的时候为了避免产生可能的复杂线程安全问题,将线程简化了,所以导致只有一个分配器,这样导致基本无法实现动态换乘;从另一方面来说,在单个电梯的调度方面,我认为自己的架构还是很好的,无论是修改某种电梯的属性,针对某一个模式的策略优化,都能够实现开闭原则,可拓展性良好。

锁和同步块的设计

第三次作业由于增加了不同类型的电梯以及换乘部分的功能,所以在分配器中新增加了每一种电梯所对应的请求的数目,当当前种类电梯所对应的请求数目为0的时候,根据待完成的请求数目是否为0来判断是等待还是结束线程。这样能够保证在换乘的时候,不会由于已经结束输入导致某一部电梯错误的执行。关于请求数目的读取和操作方法也都加上了锁。其余部分基本和第二次作业一样。

关于同步块的设计基本上和第二次作业一样,有所不同的是之前只有一个共享的请求队列,但是现在还有另一个某种电梯对应的请求数目。关于这两个变量的操作都加上了锁。

锁与同步块之间的关系也和之前一样,要么是操作了可能被共享的变量,要么是需要唤醒被锁住的对象或者需要在对应的锁的部分被阻塞。

调度器设计

第三次作业的调度器和之前有了较大的差别,在前两次作业中我基本是没有对总的请求调度器做过多的约束的,基本都是电梯主动去抢请求,但是在第三次作业中总的调度器全权负责对请求的分配。下面来详细说说第三次作业中调度器的设计。

首先是第三次作业中总的调度器设计,在总的调度器中,我对乘客的请求做了静态分配,并给出了不同的换乘策略以及对应不同换乘策略的换乘楼层,这样一来每一个请求到达请求队列之后,它被分配的电梯就已经固定了。

在电梯内部的状态转移调度器中,添加了换乘的功能。也就是电梯在送出乘客的时候,需要判断是换乘的乘客还是到达目的地的乘客,如果是到达目的地的乘客则需要对总的请求数目进行求改,否则需要将乘客放出并且对请求队列以及不同种类电梯的请求数目队列进行修改。

自己的bug

在强测和互测中都没有出现bug。但是在提交之前自己本地的测试中换乘的部分出现了问题,也就是换乘之后应该接乘客的电梯没有被唤醒导致整个程序无法结束,根本原因也是notify的部分没有处理好,增加了一些notify之后解决了问题。

发现别人程序bug所采用的策略

本次作业的互测也是使用了自动测评机,效果较好。在自己电脑上运行的时候发现同组的某个同学在night模式会出现exception,进而导致RTLE,但是交上去之后并没有hack成功,不知道是评测机的问题咋了...然后自己交上去的另外一组数据,本来想hack的是A同学,但是最后hack了R同学,也是一个死锁的问题。现在也不是很明白为什么,可能是多线程无法复现的特性把。


心得体会

  1. 写测评机是一件很有意思的事,在写测评机的过程中我又学习到了很多新的知识,比如python的多线程的很多相关知识,还有直接用cmd编译带包的java程序,还有定时投送的输入方式。感觉特别有趣。另外我个人发现测评机的开发也是迭代的,在第一周写测评机的时候有点累,但是之后每一周的测评机只要修改一些些地方就行,感觉十分有收获。
  2. 多线程程序发现bug还是挺容易的,一般一组强一点的随机数据就能发现,但是具体定位bug则是一件麻烦的事,直接调试我个人发现似乎完全没用,得在很多地方System.out才行。
  3. 出现轮询(CTLE)的时候可以在自己的无限循环部分增加输出来判断,而出现RTLE可以在自己的状态转移的部分增加输出来进行判断,或者在自己wait的部分进行输出来判断。
  4. 多线程的hack是一件很玄学的事,有时候在本地跑出来不对的地方交上去就是hack不出来,可能是因为bug复现的概率很很小。在测评的时候我们房间有一个人一口气提交了十几次才最终hack成功了一次,感觉挺有意思的。
  5. 线程安全是多线程很重要的一部分,在设计架构的时候就要顺便把一些共享变量以及相关的方法给想清楚,如果边写边想的话很容易出现奇奇怪怪的bug。
  6. 关于多线程程序的架构,在上课的时候老师讲了很多架构,和本次作业最契合的应该还是生产者消费者模型。有关黑板模型以及任务驱动模型其实也都能够运用到本次作业当中,但是似乎大家都没有用(可能是我自己没咋仔细看)。
  7. 关于架构的一些体会:有时候不是越复杂的架构就能够获得越好的效果。如果一个相对简单的架构(比如这三次作业中,我都只采用了一个托盘类来实现请求的处理,包括分配请求以及电梯抢人)能够实现同样的功能,并且还能够避免一些可能出现的线程安全问题,那我认为这个简单的架构也是可以采取的。
posted @ 2021-04-23 14:54  zymmm  阅读(73)  评论(0编辑  收藏  举报