我永远爱着OOP——第二单元作业总结

第二单元的电梯真是愉♂快呢,多线程编程作为java编程OOP中的重要组成部分,通过这一个单元的学习,我也是有了很多全新的认识

那么下面就先例行一下公事


三次作业分析

第五次作业

设计分析

实现的电梯是很简单的,没有复杂的逻辑,主要目的应该也是帮助同学们入手多线程编程,加上课上对设计模式有所点拨,所以整体的设计应该是不难的,编码量也不大,只要处理好锁的关系和waitnotify的时机,不要出现死锁,基本是不会有什么问题的

下面给出我的设计,也就是中规中矩的生产者-消费者模式

我永远爱着OOP——第二单元作业总结

这里我封装了自己的一个容器Requester(感觉什么东西加上个"者"后缀就很厉害呢),其实就是一个请求队列,实现了一下迭代器接口(后来发现不好用

然后就是电梯线程和电梯线程,调度器线程没有启用,这个设计主要是为以后的扩展做了一点准备,所以这里的Scheduler是个静态的

然后差不多就是这样了,邻接资源就是那个队列,使用的时候锁一下就好了

一个需要注意的地方是怎么控制输入结束的全局持续通知

这里是借用了信号量的思想,也就是设置了一个相应的属性,然后每次循环的时候查看一下这个信号,别的就没什么了

下面是整个程序的一些度量(忽略这只手)

我永远爱着OOP——第二单元作业总结

因为有各种各样的输出语句,所以Elevator类的方法好像有点多了,不过我确定一定很短,很简单

你看

我永远爱着OOP——第二单元作业总结

方法的复杂度都很低,看起来还是可以的

测试

对于多线程的程序我不是很清楚怎么做单元测试,单元测试也只能保证模块功能正常,但是我们都知道,真正的问题往往出现在线程的控制上的。

分享课上同学们分享的也都是各种黑盒测试(个人是比较鄙弃全部依靠黑盒测试的行为,盲目的随机胡测也许很有效,但是绝不优雅),所以也欢迎大家分享如何富有逻辑地进行测试。

第六次作业

第六次加上了捎带规则,加了点楼层,加了负层,0层神秘消失(误),整体来说笔者的设计是这样的。

三次设计的思路都是坚持做到单一职责,毕竟分割职责可以在做修改和扩展的时候放心大胆(我一向很大胆,其实是因为分隔职责后便于测试,良好的测试是修改的保险丝)。

所以就是

  • 输入线程只管输入,是一个无情的指令接收机器
  • 电梯线程只管执行,是一个无情的指令执行机器
  • 调度线程负责调度,完成(无情地)指令分发的任务

然后这样的设计就出现了

!我永远爱着OOP——第二单元作业总结

相比上次的设计,没太多变化,就是调度器动起来了,然后肩负起分配指令的任务

然后电梯中需要有一个队列维护自己持有的指令,线程变成了单步执行,每层看一看有没有上下开关门事件发生,逻辑不算复杂

调度器就是不断尝试将指令放入电梯中(后来笔者发现这里设计的不好,调度器空转太多了,加一个waitnotify机制会更好,或者是join,不过想来想去,抢占式或许是最优解,这个后面再说)

然后下面是整个程序的度量

我永远爱着OOP——第二单元作业总结

我永远爱着OOP——第二单元作业总结

可以看到调度器的复杂度比较高,其实主要是笔者作死的一个设计

笔者为指令和电梯运行加了一个方向的概念,因此自己给自己多设置了一层逻辑,没什么意义,也就是有MainRequestDirection两个量都表征运行方向,每次更新都要更新俩,判断的时候用哪个也说不清,确实是很糟糕的设计,笔者在后面也把这个量给废弃了

测试

关于这次的测试,笔者给出一些个人思考的可能出现问题的情况

  • 对于LOOK玩家,在电梯折返,或者说换向的时候打断,可能是一个边缘逻辑
  • 对于开关门和来指令的并发情况
  • 对于错过的指令的测试

第七次作业

设计分析

据说这是OOP作业难度的顶峰

鬼畜多电梯、负载、速度、随机突发随机指令,这几个情境下设计一个好的算法确实不容易

所以与其苦心分析设计,不如"实践检验真理",也就是通过本地测试时间来决定算法的性能(真是个小机灵鬼

所以在说我的设计之前,我想理性(瞎胡说)地分析一下这次调度算法的考虑

1. 关于接人和方向

(笔者很讨厌上楼的人在电梯下行的时候就上电梯),在本次作业中,出于载荷的考虑,也是加上同向捎带的判断,电梯的性能应该会好一些,但是实际检验发现,这个判断的效果并不明显,并且有的时候会出现负优化,个人分析原因是在于40条指令,三个电梯的总容量有20多,所以即使面对40指令并发,对于电梯们来说,工作量也不是很大。

正因如此,像我们考虑的情景——“很多反向请求的人上电梯,导致同向的请求上不去”,其实发生率也不高,所以在这种情景下,大吞吐反而是很优的。

2. 细节优化

对于这个评测方式,其实很多细节优化的效果也是不错的(只指分数,不保证绝对时间差),比如电梯的选择顺序,是快的优先还是随便来;是近的优先还是随便来。这种细节上的选择也会有不错的收益。

3. 抢占式很重要

如果让调度器主动分指令,电梯就要记着一会要接谁,即使这个人不在电梯里,出于安全考虑,电梯也必须为这个人留个位置,那么在这种情景下,电梯一般是一直装不满的。

本人的算法基本是硬调度,没有动态调度,调度规则都是硬编码的。(不像那些大佬们在电梯运行的时候动态通信)

我永远爱着OOP——第二单元作业总结

程序的逻辑就是每次楼层改变和进出人后就去中间的Commander那里pull一下,看看有没有自己能拿走的,这样可以保证最大利用电梯容量

然后pull的逻辑是在Commander中实现的,Commander获取电梯的当前状态,然后判断队列中有没有他能拿走的

下面是程序的度量

我永远爱着OOP——第二单元作业总结

看起来Commander好像不太高兴了,因为笔者不小心把指令分割的逻辑放到了Commander里面,这个是违反单一职责原则的,我在下面简单做一下拆分

我永远爱着OOP——第二单元作业总结

可以看到,加上Spliter后,Commander的复杂度明显下降,此时Commander中只剩下和分配有关的逻辑

我永远爱着OOP——第二单元作业总结

方法的复杂度还是很不错的

测试

这次测试笔者主要关注了下面这几个点:

  • 载荷,要保证不能超载,对于电梯载荷上限的数据要进行测试
  • 需要拆分的指令,对于需要拆分的指令,特别是可以多次拆分的指令,要重点测试
  • 效率,最好在测试的时候单步输出一下每时刻每座电梯中的人数,确保电梯有较高的载重

好了,我完事了

上面就是本系列作业的设计了,下面说一些我想说的

相比于上个系列作业,这个系列突出的需求增加更加明显,也更加友好,不存在说两次需求相互矛盾的情况,对于设计优秀的同学,每次作业的编码量肯定是不大的

然后吧,这个时间效率这个事情,我觉得确实是一个摸索的过程,群里有人在说用NN,问题不大,其实我上面说到的自己去尝试算法,然后调整算法,其实就是个手动NN的过程(误),真正要是在实际中解决这种鬼畜电梯的问题,就是要靠NN来实现根据过去推将来的,用NN来实现对未来的分析预测,从而调整自身调度策略,获得最优调度

下面想重点说一下这个监听者模式,老师上课提了很多次,下课也听同学们有讨论,所以说一点个人的见解(以下都是胡说,理性参考)

监听者模式的提出,是为了能够让一个个的listener动态地加入监听和退出监听,同时,在Commander新消息发布的时候,可以通知所有监听者,javaSwing库就大量使用了这种设计

针对本次作业,笔者最开始是考虑过用这个模式的,每当输入一个指令就通知一下电梯,然后电梯取指令,刚好很多电梯,然后可以都注册为listener

但是存在以下几个问题:

  • 所谓通知,在这里的体现就是把指令放进去,但是,在指令来的时候,电梯可能正处于上锁状态(例如,进出处理中),这时我们的Commander试图尝试往每个电梯里放指令,就会遇到锁
  • 其实上面那个还不是最重要的,毕竟只是要等一下下就好,问题是当某个请求无法被任何电梯处理的时候,这个请求就要被存起来,这样的话,其实就又退回了我们的生产消费模型了(毕竟在Swing中,如果一个消息没有监听器,这个消息是会被扔掉的,但是显然我们这里不能这样)

所以,这个模式虽然很美,但是放在本系列作业中也许有些不当之处,当然,这只是我浅薄的认识,如果大家有更好的实现方式,也欢迎在下面留言,一起讨论,一起学习

上一篇:Commander One Pro for Mac(文件管理器)


下一篇:c – 获取右值的地址