OO 第二单元总结:调度祭天,法力无边

心得体会

早春的风沙涤荡着京城上下,杨柳的毛絮洗刷了校园内外,无边的任务积攒在自己身上。

尽管世人可能不是都清楚这一点,但是时间和精力对于我来说是相对有限的。繁复的任务夺走了我沉思的时间,满日的奔波枯竭了我灵动的精力,剩给自己的便只剩浮躁的碎片化知识面扩充,和不免带来负罪感的忙里偷闲式的娱乐。

有的时候如果一些东西让你烦心,不如试着直接把它抛到九霄云外。

第五次作业

就算是一万丈那么高的楼阁也要从平地开始建造,这是亘古不变的道理。就连西方的学者,在开发最精密顶尖的技术时,也需要从所谓的基线开始。对于电梯系统来说,这样精髓关键的步骤,难道不正是框架设计吗?我深深地认为这样的认识是合理的。自打少年时光,尽管那时的我还未进入高等教育学府,我在完成任务的时候,便没有不先想清楚各处实现方法便鲁莽开工,直接上手开始写代码的。如此这般,每每到了被指派任务的时候,必然先将其放在脑海中晃荡一会。将其与脑浆混合均匀,下到碎片化思考的锅中,两面炸至金黄后出锅。这时候,一般来说也快到预先设定的期限了。然后这时的我便像考试结束铃响还在奋笔疾书的考生一般,用必死的精神试图在铃结束前写完答案那样,充分运用自己的聪明才智和编程基础,试图把这样一份成果呈递到甲方面前。

有朋友听说了这样那样的我经常在做的事情,惊奇道:“不愧是 ptw,轻易地就做到了我们做不到的事情!” 我非常感谢他们对我游走在截止期限边缘的勇气的认可。我听说,久经风霜的战马能够找到归乡的路径,手刃千牛的庖丁可以定位牛身的脉络。然而熟悉的事物容易被人们忽略,最为危险的境地反而是最佳的容身之处。经常在河畔、在隰地边行走的人,难免偶尔会把鞋子沾湿。学业的隐患,也这样藏在了日常的生活节奏之中。在面向对象的第五次作业中,使着一手老革命的工作方法的我,就这样遇到了新问题。一天乃至半天即可解决面向对象作业的印象,自预习任务产生,由第一单元的三次作业加深,在本单元甫一开始时尚未显现出其危险性,但使我直到周日才发现:自主设计电梯调度器并非一朝一夕之功,而需瞻前顾后之劳。

就像统领千军的将军所持有的虎符一般重要,在电梯系统设计的指导中,没有不出现调度这个概念的。从高加索山到西西里岛,从课上的实验代码到评论区的伙计们,尽管一千个设计者就有一千种电梯系统,但仿佛没有了调度器,便无法设计电梯系统似的。对此深刻的认识使我不得不开始着重思考调度器的设计,并在一开始便将其作为我的电梯设计的重要组成部分来思考。

然而精疲力尽的我实在是整不动活了,所以经过日日夜夜的思考,最后我不得不采取长话长说的方式,以更贫瘠的文字来表达我的意思。

周三到周六我一直在纠结调度器和电梯之间的交互形式。是给电梯发上下开关的指令流,还是直接指定目标楼层让它运行到指定点,还是给电梯指定运载请求?如果电梯接受指令流,那么电梯的逻辑就相对简单,但调度器就需要动态获取电梯状态。电梯状态是动态变化的,如果让所有电梯在调度器执行时阻塞,那么调度器就大大地增加了系统运行时间,大大地削弱了多线程的并行性。如果电梯不阻塞,那么调度器获取到的电梯状态就是不准确的,有可能跟实际情况不一致,增加了保持指令正确性的难度。如果直接指定目标楼层,首先在状态上会面临同样的问题,而且电梯也需要自己管上下人,那么究竟载上谁也难以由调度器控制,因为这要求调度器追踪电梯到达每个层(就是电梯到达某层时均需要唤醒调度器)。如果给电梯指定运载请求,我们的电梯、调度器、请求池之间的耦合又相当强,而且调度器需要掌握所有人和所有电梯,在这之上做最优化需要相对复杂的逻辑。尽管我觉得大概率上是因为我自己比较傻想不到怎么去优雅地实现这些逻辑,但这让我觉得十分地剪不断理还乱,没有多线程和面向对象的味,反而充满了玄学调参优化的气息。

终于,在纠结于几种交互粒度之间的来来往往之间,宝贵的工期流逝殆尽。尽管数位助教送上了暖心的关怀,但我还是没能选择出一个最佳的实现方式来顺利地把任务完成。这也成了我面向对象这门课中第一次的无效作业,使我对我对多线程编程思想的理解程度的信心又动摇了几分。

第六次作业

  • 159 行
  • 6.52 KB
  • 强测 99.3262

调度器设计

本次作业中没有调度器。

在经历了第五次作业无效的惨痛后,我痛定思痛,但关于调度器设计还是没有什么比较好的主意。

正当我山穷水尽疑无路的时候,柳暗花明之处,一些奇特的想法开始涌现。

如果我们把调度器(Scheduler)扔掉?

我们直接让所有电梯自由竞争所有用户需求。人一进来,电梯们一起蜂拥而上,具体谁抢到,就交给 JVM 决定吧。

电梯运行逻辑类似常见电梯(听说这叫 LOOK?),能载则载,自觉干活,充分发挥集体经济的力量。

拿掉调度器后,项目进度推进神速,代码逻辑酣畅淋漓,真可谓是一日千里。

如果说这套东西里还有什么是起到调度作用的“调度器”的话,那可能只剩 JVM 了吧。

同步块介绍

// Elevator.java
synchronized (pool) {
    if (pool.isEmpty() && persons.isEmpty()) {
        if (pool.isEnd()) { return; }
        try { pool.wait(); }
        catch (InterruptedException e) { e.printStackTrace(); }
    }
}

// InputThread.java
synchronized (pool) {
    pool.addRequest((PersonRequest) req);
    pool.notifyAll();
}

// RequestPool.java
public synchronized void addRequest(PersonRequest req) { pool.put(req.getPersonId(), req); }

public synchronized Stream<Integer> extreme(int dir) {
    return pool.values().stream().map(u -> dir * u.getFromFloor());
}

public synchronized PersonRequest[] getReq(int floor, int dir) {
    return pool.values().stream().filter(
        u -> {
            int f = u.getFromFloor();
            int t = u.getToFloor();
            return f == floor && dir * (t - f) >= 0;
        }
    ).toArray(PersonRequest[]::new);
}

public synchronized ArrayList<PersonRequest> getReq(int floor, int dir, long cap) {
    PersonRequest[] qwq = getReq(floor, dir);
    ArrayList<PersonRequest> ret =
            new ArrayList<>(Arrays.asList(qwq).subList(0, (int) Math.min(cap, qwq.length)));
    ret.forEach(u -> pool.remove(u.getPersonId()));
    return ret;
}

使用的锁只有请求池的锁。锁使得对请求池的关键读写操作具有原子性。

第七次作业

  • 171 行
  • 7.47 KB
  • 强测 99.9072

同步块介绍

// Elevator.java
synchronized (pool) {
    if (pool.extreme(0, reachPred).count() == 0 && persons.isEmpty()) {
        if (pool.isEnd()) { return; }
        try { pool.wait(); }
        catch (InterruptedException e) { e.printStackTrace(); }
    }
}

// InputThread.java
synchronized (pool) {
    pool.addRequest((PersonRequest) req);
    pool.notifyAll();
}

synchronized (pool) {
    pool.terminate();
    pool.notifyAll();
}

// RequestPool.java
public synchronized void addRequest(PersonRequest req) { pool.put(req.getPersonId(), req); }

public synchronized Stream<Integer> extreme(int dir, Predicate<Integer> pred) {
    return pool.values().stream().filter(u -> pred.test(u.getToFloor()))
            .map(PersonRequest::getFromFloor).filter(pred).map(u -> u * dir);
}

public synchronized PersonRequest[] getReq(int floor, int dir, Predicate<Integer> pred) {
    return pool.values().stream().filter(
        u -> {
            int f = u.getFromFloor();
            int t = u.getToFloor();
            return f == floor && dir * (t - f) >= 0 && pred.test(t) && pred.test(f);
        }
    ).toArray(PersonRequest[]::new);
}

public synchronized ArrayList<PersonRequest> getReq(
        int floor, int dir, long cap, Predicate<Integer> pred) {
    PersonRequest[] qwq = getReq(floor, dir, pred);
    ArrayList<PersonRequest> ret =
            new ArrayList<>(Arrays.asList(qwq).subList(0, (int) Math.min(cap, qwq.length)));
    ret.forEach(u -> pool.remove(u.getPersonId()));
    return ret;
}

使用的锁只有请求池的锁。锁使得对请求池的关键读写操作具有原子性。

调度器设计

本次作业中没有调度器。

电梯直接从请求池主动获取请求并完成运载工作。

架构设计分析

其中请求池并不作为线程存在,只是为了方便展示其与其他部分的交互情况而特地表出。

Bug 分析

印象中我在公测和互测中没有出过 bug。

只记得有一次 WA 是因为忘了加上电梯 ID 输出。

Hack 策略

我没有 hack 别人。

我只是把他们的代码下下来随便跑了跑。

其中有一份代码运行时间是我 3 倍多,可惜叉不掉 = =

重构经历总结

除了开始纠结架构,基本没有重构过。

之后为了稍微节省一点代码行数,把一些模块化的函数给内联化了,这是为了整活而做的一些比较牺牲优雅性的小规模重构,不值一提。

心得体会

写在开头了。

posted @ 2021-04-26 01:38  葡萄味柠檬茶  阅读(643)  评论(1编辑  收藏  举报