OO第二单元总结
写在前面
第二单元的调度算法作为学习多线程的起步任务可以说是比较合适的.第一次作业的捎带调度对于算法没有很高的要求,并且只有一部电梯,对于初步接触多线程调度的初学者(我)而言,能够集中注意力在线程通信以及同步,掌握线程安全的基本知识;第二次作业也没有大的需求变化,在现有基础上迭代开发一个总调度器就能够实现;第三次作业则要求调高了比较多,主要的区别在于,利用单部电梯实现的前两次作业在这里行不通了,必须要考虑电梯之间的连携,这就需要拆分需求以及保证前后之间的同步,对于同步控制以及调度算法都有比较高的要求.
作业分析
第一次作业
结构
时序图
(其中Elevator和Dispatcher通过互斥锁以及共享内存实现实际上的进程同步)
核心想法
主线程设计:
(1)激活其他线程
(2)持续读取输入:一开始没有多线程的思想,对于堵塞输入还很纳闷,这样不就得要整一个程序堵着了吗?与同学探讨之后才明白,堵塞的进程会自动调度走,不需要认为调度.因此,直接让主程序在结束之前跑循环就好了,读到输入就直接通过RequestQueue(tray)这个线程安全的托盘输入就好.
(3)结束线程:作为最初始的线程,有必要通知其子线程结束
调度器设计:
(1)数据结构:内部保持两个队列,其一是等待队列(在电梯外等待的队列),另一个是运输队列(已经在电梯内的队列).
(2)采用look算法:在运行的时候保持目的楼层为当前方向上所需要到达最高的楼层,具体实现是通过比较等待队列的最大起始楼层以及运输队列的最大目的楼层,在每次队列有更新的时候刷新目的楼层;在已经达到最高楼层后,需要寻找最低楼层.这样,通过尽量少的往返,在效率上有不错的表现,在代码逻辑方面也很少有需要特判以及考虑边界情况,鲁棒性很好.
(3)同步设计:与Elevator持有同一把锁,实现了两个线程互相唤醒等待,达到每个楼层访问一次调度器的目的.
电梯设计:
(1)与老师所说的设计不同,我的设计中,电梯有点像idiot类了,只是负责基本的输出,以及根据调度器的信息上下行一层楼.
第二次作业
结构
时序
核心思想
主调度器(MainDispatcher):
(1)需要主要这次的请求是有人数限制的.
(2)相较于上一次作业,这次作业主要改变是,有多部电梯调度问题,需要设计子请求队列以实现对于不同电梯的乘客分派.实现算法需要兼顾效率和公平,我的实现思路是在不超过人数限制的时候,尽量将乘客分配给同一个方向上的电梯(并且是没有满员的),不行则调用空闲电梯,再不行则根据人数以及相距乘客的距离,方向等参数,得到加权的分值,再根据分支高者进行分配(兜底用).
这次作业我还经同学启发写过另一个算法,就是直接模拟每个电梯加入后的预期结束时间,并选择最小的放入.事实上这种做法并没有很大的作用(可能老师所说的将与其时间尽量平均的算法效果更好),想来可能是在于局部最优并不总能够实现整体最优.当前的情况下时间最短并不代表在加入多个后还能够保持整体时间最小.由于请求的不可预测性,这种算法的作用存疑.
第三次作业
结构
第三次作业整体框架没有很大变化,但是在框架内的线程,特别是主调度器线程有比较大的变化.
时序
复杂度
类大小
核心思想
指导原则:
(1)不换乘优于换乘(能用一个电梯用一个电梯)
(2)换乘采取静态策略(动态策略需要频繁访问电梯情况,并且相对复杂;另外,局部最优并不代表全局最优)
(3)电梯之间对于换乘的存在是可以感知的.(即换乘接力的电梯之间知道有换乘的请求存在,并将换乘的请求纳入调度考虑之中,不能等到需要换乘的乘客下电梯后再通知).
MainClass:
(1)新增需求:能够创建电梯进程动态增加共享数据,此时需要特别注意新增电梯时新增共享变量的控制,原子性之类的,防止在创建的过程中被其他线程访问而相应的初始化还未完成.
主调度器:
(1)新增功能:任务的分解,不同于之前只有单个任务的分配,现在需要根据任务的请求以及电梯的运行情况将需要拆分的任务分配给合适的电梯.
(2)需要修改的地方:现在需要将不同电梯根据类型来分,以获得相应类型电梯的情况.
(3)调度算法的变化:优先选择可以单独完成任务的电梯,此时的电梯选择可以复用前面的算法;在必须要合作的电梯中,根据以往的算法得出最优的首部电梯,然后根据所能达到的最近楼层选择另一类电梯(最近不一定是最优的,也许根本不是最优的,但是,静态策略就不用考虑复杂的其他电梯的情况,鲁班性好,简单可靠,事实上效果也不错),再从可以选择的电梯之中复用之前的选择算法,择优选择.
(4)共享变量的变化:信息传递主要依赖于共享变量,共享变量此时应该根据电梯的种类进行分类,方便分派访问.
子调度器:
(1)最主要的改变是要接收还没有到达的换乘乘客的请求并且与现有请求一起安排调度算法.我的主要实现思路是,将未到达的请求与在普通的未进入电梯的请求同等看代一起调度,在完成主要的任务之余经过所换乘的楼层查看乘客请求,当未到达时继续当前任务,以免当前任务的乘客一起等待.需要注意的是,对于多个未到达请求而没有其他实际任务的情况,考虑到楼层差距比较大,不应采取电梯轮流访问,而是应该停止在某一楼层等待,否则容易造成很长的空转时间.
基于SOLID设计原则的评价
SRP(单一职责):
主线程:主要有读取需求并且分配需求的作用,还有终止线程的作用;这次将分配需求,创建线程,停止的功能都集成都其中,感觉有点太大了.应该将创建线程分配给工厂类.
两个调度器:坦白说,这方面设计不好,两个调度器的线程代码都直逼500行,主要是由于算法比较多,应该将这类控制类仅仅作为控制类使用,将算法分离出去.
电梯线程:主要是负责调用sleep控制上下楼时间,在主要线程比较复杂的情况下,有点像idiot类.
OCP(开闭原则):
这次做得比上一个单元好,主要的子调度器算法没有大的修改,主调度器也是通过增加算法来实现新的调度分配.
LSP(可替换原则):
由于这次作业没有涉及太多继承的问题,我的设计中也没有考虑太多子类继承的问题,但从实际效果的角度来看,主要的类中,只要子类没有改变算法的需求,新增功能或者属性后,应该都可以实现替换.
ISP(接口分离):
本次作业如果说哪里可以使用接口,可能算法部分是不错的选择,可以用算法接口封装,但是考虑到耦合性的问题(需要用到许多共享变量),也不一定是有效的方法.
DIP(依赖倒置):
同样由于缺乏明显的继承层次,甚至不同的电梯类也可用不同参数表示而行为上没有明显的差异,因此没有明显的接口设计.但是,在有抽象参差的时候,还是要注意将高层次的类主要依赖在接口而不是具体的类上.
BUG分析
自己的bug
在三次作业中,我自己的bug在第一次作业中出现,但是,到现在为止,这个bug的产生原因还是比较迷.主要是在于这次的bug并不是实质上的逻辑或者什么错误,而是在于在没有请求输入的时候,我选择将控制器线程和电梯线程轮流相互唤醒,以尽量实现快速反应(尽管实际上可能会因频繁唤醒导致浪费CPU时间).在中测中,由于请求的安排比较密集,因此没有重大的问题.但在互测中,由于有相隔时间比较远的请求存在,出现了100%复现(RE)而本地测试100%通过的情况.在经过同学的提醒后试着改了一下才通过.但是,这种错误出现CTLE可以理解,但是RE又是怎么回事?但还是在此提出,希望得到惊醒,不要频繁调度线程,最好有一个sleep的时间,否则怎么错的都不知道.
别人的bug
在前两次作业中,别人的bug大多都出现在密集输入的时候.因此,在本地的评测机上可以采用同时输入上限额的信息,这种情况下丢失信息可能是没有处理好调度器和电梯之间的分派关系.
本次测试策略与上一单元的测试相比较,最大的不同可能就是对于边际数据的测试.这次明确规定的输入时间,使得边际数据构造失效,而要真正着眼于程序整体的鲁棒性和线程安全问题.
心得体会
线程安全:
现阶段的线程安全问题总的来说就是锁的使用问题.
所有访问公共变量的,必须考虑怎么上锁,上哪个锁,并且统一规范的问题.
所有的读写都可以只对容器中特定的对象上锁,而无需整个容器上锁.这样,在新建共享对象的时候,可以通过锁保证初始化过程的原子性(中间过程不被其他线程读写);在读写的时候上锁,也可以保证原子性(甚至在存在可能多个读的情况的时候可以上读写锁).
在锁的生命周期中,要考虑休眠的时候,是sleep带锁休眠,还是wait让锁休眠.使用锁的时候要在循环内多次申请锁,还是在循环外申请锁,在循环内多次让渡锁的使用.因此,锁的作用范围也很有讲究.
设计原则:
(1)尽量少的线程间交互:线程间交互需要大量用到锁,一个不好死锁不说,还有可能多次被阻塞导致效率低下.因此,从鲁棒性和简洁明了的角度来说,能少交互少交互.因此,在我看来,静态策略由于动态策略.
(2)还是面向对象的层次化思想:无论从底层到上层还是从上层到底层,不同层次之间的类或者方法不是我现阶段要考虑的问题,只要把方法放那,怎么实现是我那个阶段的事情,这样可以简化现阶段的思考.
(3)线程能不动不动:线程轮循不好,一次没有实现目标,应该休眠一下让渡使用,不要空占资源(否则给你10sCPU都跑不完)
(4)好的层次设计比极端的优化好得多:好的层次设计在后续的变化中可以有效减少重构负担.与之相对的,极端的优化可能就是针对问题改变架构,对于当前可能有一定的优化,对于未来却难以迭代.
(5)属性不要固化设计:不要将方法与某些属性(特别是定值的属性)绑死,面对可能的变化,方法要尽可能面对不同的属性,并且有可扩展性.
(6)调试多线程用输出是很好的办法:对于所有过程中可能出现的错误情况都设置一个if来判断并输出,不要担心测试时可能会有错误输出(毕竟那时候你铁定错了),这样这多线程debug的时候很有效.(还可以设置checkmode,在不同的过程中输出状态变化的信息,这样开关方便,deubg中掌握线程状态的变化很有效).![]