OO Unit2 Elevator Scheduling
本单元三次作业架构相似,整体采用生产者消费者模型,这种设计模式服务于本次作业三个主要特点:支持并发、输入输出解耦合、线程安全易保证。
-
支持并发:多线程的优势在于并发提升运算效率,当涉及到多任务处理时,通常可以将主要部分使用多个线程,配以互斥锁/信号量/条件变量来实现同步块/原语/临界区来解决竞态条件、线程间通信等问题。
-
输入输出解耦合:输入输出位于两个线程优势还在于解耦合,在该抽象层次上保证了单一职责
-
线程安全易保证:在该模型下主体逻辑中,中间托盘类作为唯一的共享资源,是唯一需要保证线程安全的部分
hw5
层次化设计
结构较简单,仅展示电梯时序图
主要工作量在于电梯内部职能的实现。电梯的行动是归为四状态有限状态机,状态转移由电梯自己的dispatcher确定,电梯本身只负责执行,以此实现机制与策略分离。
策略与机制
-
策略:look,优先接最远
-
机制:量子电梯
优势主要在两方面:
-
充分晚决策、充分扩大了接受可受理请求的孔径时间(而没有消耗额外的移动时间)使可受理请求尽量不会被落下
-
移动时间被充分缩短,实现瞬移
前者是完全正向优化,通过在移动过程中可以原地开关门接人实现;后者是与数据相关的不完全优化(随便起的名字),通过halt后第一次移动瞬移实现。
-
这部分线程安全问题,只需要在托盘类上都加锁即可,关于量子电梯的实现流程大致如下:
synchronized (pool) { while (System.nanoTime() - startT < 200000000) { pool.wait(200 - (System.nanoTime() - startT) / 1000000); chosenGot = pool.get(block).size() > 0; if (chosenGot) { break; } } }
(经小伙伴指出,不用nanotime可能有极端情况下ms进位问题导致精度无法保证
对于halt后瞬移,我开始持怀疑态度。在充分压缩移动时间的同时,也充分压缩了可受理请求的孔径时间,容易落下人。
后来用hw5部分强测数据做了测试:
无halt后瞬移 | halt后瞬移 | |
---|---|---|
1 | 76.9290 | 76.9500 |
2 | 78.1930 | 77.7820 |
3 | 77.3790 | 76.9960 |
4 | 76.9870 | 75.8280 |
5 | 68.5880 | 68.1640 |
7 | 71.7580 | 71.2010 |
10 | 54.3930 | 54.0150 |
17 | 96.2860 | 96.1190 |
20 | 106.7980 | 105.7200 |
发现halt后瞬移对大部分场景是正向优化。这告诉我们,等一个不一定等得到的人不如早点离开去追求下一个(
具体实现逻辑请看zsm学长博客:https://www.cnblogs.com/kasuganokusa/p/14701724.html
量化指标
强测得分:99.9993,互测被捅11刀
清明节玩high了,没看讨论区不知道输出时间戳需要递增。看到强测没挂点,我的心里只有感恩。
hw6
层次化设计
横向电梯时序图:
本次新增了横向电梯。主要业务和纵向电梯很相似,可惜由于checkstyle把protected ban了,通过继承实现代码复用还需要大量的getter,属实蛋疼,遂不继承。
策略与机制
对于横向电梯设计如下:
-
策略:电梯为空则停摆,根据两侧请求数量确定初始移动方向,根据接到第一组人再次确定移动方向(也是不完全优化,若不做则类似接B-A需要转一圈,蛋疼),移动起来且电梯内有人则不转向,所有请求都受理(考虑到环形,转向使前功尽弃,应尽量避免)
-
机制:加上halt后瞬移的量子电梯
量化指标
强测得分:99.5655,互测被捅1刀
虽然没有写评测机,但自己还是大概测了测的。最后只被一个哥发现了问题,我的心里只有感恩。
问题出在电梯原地空接后量子电梯用到的startT没有重置导致移动过快。
hw7
层次化设计
Controller时序图:
纵向电梯时序图:
新增换乘。可以看成边权为移动时间,点权为等待时间的无向图然后跑最短路径。不加额外限制的最短图允许多次换乘,与请求横向分量一次消除的利弊如下:
-
横向一次完成最坏场景是到1层换乘,等待时间最多是一层横向等待时间(业务不繁忙时移动时间消耗占主导)
-
允许多次换乘则面临多于一次的纵向等待时间、开关门次数带来额外的时间消耗
需要trade off,考虑到纵向平均等待时间>>横向平均等待时间,采用最少换乘次数,横向一次性消除请求的横向分量的策略
策略与机制
策略上,待定参数只有换乘楼层,关键在于换乘楼层的选择。
官方给出的换乘楼层确定的方式较为简单,通过移动距离静态估价,作为粒度为1的估价方案给出。我将每block(floor)电梯个数、等待时间电梯移动速度几个因素纳入考量,基于概率设计cost函数
private int strategy2(int ff, int tf, char fb, char tb) { // 根据电梯运行速度与电梯个数,静态确定换乘层 // 2-9层上行下行2种状态,1、10层仅一种状态,若是scan在18种状态间循环遍历 // 引入扫力,定义为每秒状态步进数(不考虑步进后状态之间的碰撞),等待状态数按期望计算,竖直期望等待9状态,水平期望等待2状态(单向转) // 运行时间与被接概率均与运行速度相关,认为与扫力呈正相关(没有进行开关门修正) int res = 0; double minCost = Double.MAX_VALUE; int[] v = new int[3]; // v[0]200ms v[1]400ms v[2]600ms for (int i = 0; i < 10; i++) { v[0] = 0; v[1] = 0; v[2] = 0; for (FloorElev e : floorElevs.get(i)) { if (((e.getPark() >> (fb - 'A')) & 1) + ((e.getPark() >> (tb - 'A')) & 1) == 2) { v[(int) e.getDuration() / 200 - 1]++; } } if (v[0] + v[1] + v[2] == 0) { continue; } double cost = 0; // 横向等待 cost += (double) 2400 / (6 * v[0] + 3 * v[1] + 2 * v[2]); // 横向运行 cost += (double) 3000 * (6 * v[0] + 3 * v[1] + 2 * v[2]) / (36 * v[0] + 9 * v[1] + 4 * v[2]); if (i + 1 != ff) { v[0] = blockSpeed[fb - 'A'][0]; v[1] = blockSpeed[fb - 'A'][1]; v[2] = blockSpeed[fb - 'A'][2]; // 纵向等待 cost += (double) 10800 / (6 * v[0] + 3 * v[1] + 2 * v[2]); // 纵向运行 cost += (double) 1200 * (i + 1 - ff) * (6 * v[0] + 3 * v[1] + 2 * v[2]) / (36 * v[0] + 9 * v[1] + 4 * v[2]); } if (i + 1 != tf) { v[0] = blockSpeed[tb - 'A'][0]; v[1] = blockSpeed[tb - 'A'][1]; v[2] = blockSpeed[tb - 'A'][2]; // 纵向等待 cost += (double) 10800 / (6 * v[0] + 3 * v[1] + 2 * v[2]); // 纵向运行 cost += (double) 1200 * (tf - (i + 1)) * (6 * v[0] + 3 * v[1] + 2 * v[2]) / (36 * v[0] + 9 * v[1] + 4 * v[2]); } if (cost < minCost) { minCost = cost; res = i + 1; } } return res; }
需要指出,静态估价得出cost结果都是相当粗糙的。由于通过SCAN策略基于概率计算,look实际场景和SCAN相去甚远,等待时间估计很不精确。同时,被哪个电梯接上是可以根据电梯实时状态动态刻画的,此方案信息挖掘也不充分,同时最后的效果是有一个测试点较低,都说明该方案有较大提升空间。
较理想的估价方式应当是动态获取电梯楼层信息、开关门信息(满载看作一次Exception即可,影响不大),这些都是仅与电梯内乘客信息相关的可刻画参数。
机制上,我采用信号量的方式进行线程间通信,发送终止信号,大致过程如下:
//Elev public void run(){ if(reqFinish) Controller.accomplish(); } //Controller private final Semaphore reqLog; public synchronized void accomplish() { reqLog.release(); } public void run() { while (true) { if (req == null) { try { reqLog.acquire(reqCnt); } catch (InterruptedException e) { e.printStackTrace(); } for (int i = 0; i < 5; ++i) { blockPools.get(i).stop(); } for (int i = 0; i < 10; ++i) { floorPools.get(i).stop(); } break; } } }
每次完成一份请求V一单位Sem,在req ==null时P reqCnt单位的Sem,发送结束信号前对所有Req进行验收。
量化指标
强测得分:99.3895,互测被捅4刀
测试力度不够,因为没有密集请求更易出现冲突的notion。还是线程安全问题,考虑不周啦!TnT
心得体会
线程安全
-
临界区仅需要设置在修改共享资源的部分,计算过程应当在临界区外
-
通过信号量可以起到查收、限流等操作,是线程通信的粒度最小的不二之选
层次化设计
-
自由竞争yyds,量子电梯yyds,但细节没弄好就是在当赌徒了(
-
职权应当充分独立
-
设计模式的使用不要生搬硬套,以这次为例,生产者消费者模型有助于解耦合,单例模式、工厂模式却会徒增复杂度,工程性不强的项目还是应当具体分析(暴论