电梯的这三次作业是对并发编程的一次管窥,感觉收获还是蛮多的。在设计上有好的地方也有不足,这里简单回顾总结一下

设计总述

电梯这个问题由于比较贴近真实生活,所以需求还是很好理解的。总的来说,我的数据处理流程如下(第二次作业):

  1. 使用官方接口读入下一条请求,请求进入总调度器的消息队列
  2. 总调度器取出下一条请求并生成相应的任务,将其分配给一部电梯(输入该电梯的子调度器任务队列)
  3. 每部电梯执行当前任务队列中的第一个任务,若执行完毕(接人/卸人完毕)则将该任务出队,并开始执行下一条任务

这里的一条任务指“到某一层搭载/卸载某位乘客”。因此一条用户请求至少要分为两个任务执行(到起始层 - 接人 - 到目标层 - 放人)

进一步的,在第三次作业中涉及任务切分,一个用户请求可能要被切分成若干子请求(FROM-1-TO-20切分成FROM-1-TO-15和FROM-15-TO-1),因此任务的执行存在先后依赖关系。所以第三次作业在第二次的基础上,改动如下

  • 分配任务前先将任务拆分至必要的粒度
  • 电梯执行完一个阶段的任务后调用总调度器的回调方法,告知总调度器可以分配下一个阶段的任务。这样就实现了一个闭环控制

考虑到更换策略的可能性,总调度器中的任务分配器Distributor和电梯子调度器ElevatorScheduler分别抽象出了相应的接口。实际实现了对应Scan、Look、CS-Scan三种调度算法的子调度器和一个专门为第三次作业A电梯设计的子调度器;分配器则只实现了两种硬编码的分配策略,时间关系并没有进一步设计优化。

线程间采用的是非常经典的生产消费模型,使用毒丸处理输入终止,由于问题在并发性上并不复杂所以没有遇到特别印象深刻的并发安全性问题。

调度策略

仅讨论第三次作业。采用静态拆分请求的策略

分析三部电梯的可停靠楼层与运行速度,发现A电梯是最特殊的:-3层和16-20层只有A可以抵达,而A的速度也是三部电梯最高的。

因此直觉上认为:A是最可能成为性能瓶颈的电梯,因为它经常要在楼层两端往返。所以调度要解决的第一个问题就是,如何为A电梯划分任务。

我最终采用的策略是:将目的地是-3至-1层和目的地是16-20层的任务划分给A,其余任务由BC承担。这样A将专注于两端的任务,而又不至于过于空闲(地下层如果只把-3层划分给A,A会经常空闲)。这样A的定位就是一个较高速的“班车”。

B的泛用性是最高的,也因此B的可优化空间很高。但是由于这次没有时间实现请求的动态拆分与分配,所以直接把中间楼层所有偶数层的任务划分给B,同时把奇数层任务划分给C。

设立三个中转层:1层、9层与15层。

这个设计的性能未必很出彩,但也不至于太烂,而且易于实现,属于比较中庸的做法。

具体的调度算法,B和C电梯使用Look,而A电梯使用专门改进的Look算法,确保在15-20层上行时不装载前往低楼层的用户,在-1至-3层下行时不装载前往高楼层的用户,这样可以充分利用电梯容量。

架构设计与度量分析

这次的设计,将子调度器和任务分配器接口抽离。这样的设计主要利于优化时的算法替换,比如当我为A电梯重新设计了一个子调度器后,直接将新的构造器替代老的传入A的构造函数即可。

从复杂度来看,最复杂的部分是几种子调度器和电梯类,考虑到调度算法的确较为复杂,个人认为这个结果是可接受的。

输入、输出模块与总调度器采用单例模式,各线程间通讯采用生产消费模式。

SOLID自评

  • 职责单一原则遵循的不好,比如这次的Task类同时兼顾送人和移动到某个楼层两种“指令”,虽然没有导致太过严重的设计问题,但味道实际上很坏。
  • 开闭原则遵循的不错
  • LSP遵循的不错(主要是这次继承关系太少了……)
  • 依赖倒置似乎没有问题,架构是面向接口而不是面向实现的。
  • 接口分离有些问题,这次的调度器类直接封装了一个抽象基类,其实更稳妥的做法是先抽离接口再封装基类

Bug分析与测试策略

本单元强侧/互测均没有遇到bug,也没有捕捉到他人的bug

自己遇到的一个印象深刻的bug是,手抖少删了一行lock.lock()导致程序卡死,查了很久才看到。这个故事告诉我们,除非有特别明确的优化需求/功能需求,否则还是应该尽量用更优雅的synchronized而不是Lock(这次用Lock的主要动机是找个机会练练手……)

测试有很多可以谈的地方,包括面向正确性的测试和面向性能的测试。可以说这单元的测试更考验功底了,首先定时投入就是一个很烦人的事情,黑箱测试的门槛一下子提升了很多。

抛开单元测试,这里重点讨论如何进行黑箱测试

定时投入

首先需要解决的是定时投入的问题。最容易想到的是,测试程序也借助多线程实现向输入流的定时投放。

我第一次作业的评测机就是这个原理,通过Python的subprocess模块调用写好的java程序,利用管道写入输入并取出输出。使用time.sleep()来控制写入的时间,确保两次写入间隔给定的时间差。因此输入流程就是

time.sleep(delay_time)
popen.stdin.write(data)
popen.stdin.flush()

这三步不停循环。最后从标准输出流里取出输出即可

但是这个做法存在一个问题,即jvm的启动时间也被计算在内,再加上python的sleep并不算很精准,导致实际误差较大。因此下一个思路是,直接修改两个官方IO接口,使其可以接受带时间参数的输入。借助java自带的管道流实现数据投放。最终效果还不错。

项目地址:https://github.com/Mistariano/buaaoo-elevator-test-suit

无论哪种方法,这一步最终达到的结果为:将定时投入转化为传统的输入输出形式,方便生成测试用例,将测试划归为传统形式。

Special Judge

这次的输出特点为:正确输出不唯一,但正确性判定可以用规则固化,因此期望输出-实际输出的正确性评判方式可以用一个SPJ替代。

正确性判定规则:

  1. 电梯每次移动距离为1(Arrive是连续的,不允许瞬移)
  2. 电梯移动速度不能超出限制
  3. 电梯不能到达无法到达的楼层
  4. 电梯开门时门必须是关闭状态
  5. 电梯关门时门必须是打开状态
  6. 电梯开关门速度不能超出限制
  7. 电梯装卸乘客时门必须是开启状态
  8. 电梯到达新楼层时门必须是关闭状态
  9. 电梯载客时乘客必须在当前楼层等待
  10. 电梯载客时乘客必须在电梯外
  11. 电梯卸客时乘客必须在电梯内
  12. 电梯载客时电梯内人数不能超出电梯容量(仅第三次作业)
  13. 程序结束后所有电梯门必须为关闭状态
  14. 程序结束后所有乘客必须抵达目的地

用这套规则应该能测出所有的功能性错误,同时还成功测出了室友的线程安全问题……

并发测试

由于这次作业每次运行时长在30~60秒左右,正常的单线程测试速度会较慢,又考虑到电梯运行时绝大部分时间线程处于休眠状态,并不是计算密集型任务,因此这次引入了并发测试。使用Python的multiprocessing库实现。

心得体会

并发编程可以说是现代开发的基本功,而电梯这单元的很多内容实践的是并发编程的基本功。

回顾这三周,不少事情夹杂在一起,忙忙碌碌,导致很多之前想做的事情没有来得及实践,略有遗憾。比如完全可以自己封装一套程序,让电梯的运行过程可视化,辅助进行性能分析;比如完全可以使用一些传统算法(dp、搜索)或者一些更高级的手段(决策树、nn等)来优化调度策略——但是精力有限。

即使如此,在这单元的表现总的来说是自我满意的。相比于表达式的草草收尾,这个单元可以说是稳扎稳打了。希望接下来的课程可以继续保持,也期待接下来的更多收获。

FROM-4-TO-6!!!!!!!!! - OO第二单元总结的更多相关文章

  1. oo第二单元作业总结

    oo第二单元博客总结 在第一单元求导结束后,迎来了第二单元的多线程电梯的问题,在本单元前两次作业中个人主要应用两个线程,采用“生产者-消费者”模式和共享数据变量的方式解决问题.在第三次作业中加入多个电 ...

  2. OO第二单元优化博客

    OO第二单元优化博客 第五次作业没有性能分,但是,我在这一单元的宗旨就是写一个日常生活中 最常见的那种电梯,所以第五次我没有写傻瓜电梯,而是直接写了个\(look\),和第六次基本相同. 总计一下lo ...

  3. 【OO学习】OO第二单元作业总结

    OO第二单元作业总结 在第二单元作业中,我们通过多线程的手段实现了电梯调度,前两次作业是单电梯调度,第三次作业是多电梯调度.这个单元中的性能分要求是完成所有请求的时间最短,因此在简单实现电梯调度的基础 ...

  4. OO第二单元小结

    OO第二单元小结 一.三次作业代码分析. 1.第一次作业 第一次作业是单部电梯的傻瓜调度,由于其过分傻瓜,所以第一次作业我只有两个类,一个main,一个电梯,main类负责不断从输入流中读取命令,如果 ...

  5. OO第二单元多线程电梯总结

    OO第二单元多线程电梯总结 第一次作业 设计思路 Input为输入线程,负责不断读取请求并将读到的请求放入调度器中. Dispatcher为调度器,是Input线程和Elevator线程的共享对象,采 ...

  6. 电梯也能无为而治——oo第二单元作业总结

    oo第二单元作业总结 一.设计策略与质量分析 第一次作业 设计策略 在第一次作业之前,我首先确定了生产者--消费者模式的大体架构,即由输入线程(可与主线程合并)充当生产者,电梯线程充当消费者,二者不直 ...

  7. 2020北航OO第二单元总结

    2020北航OO第二单元总结 前言 本单元考察基于多线程的电梯调度问题,成功让我从一个多线程小白到了基本掌握了使用锁来控制线程安全的能力,收获颇多(充分体验了迷茫地de一个又一个死锁bug的痛苦). ...

  8. OO第二单元——多线程(电梯)

    OO第二单元--多线程(电梯) 综述 第二单元的三次联系作业都写电梯,要求逐步提高,对于多线程的掌握也进一步加深.本次作业全部都给出了输入输出文件,也就避免了正则表达式判断输入输出是否合法的问题. 第 ...

  9. OO第二单元作业总结【自我反思与审视】

    第二单元作业的完成史,就是一部心酸的血泪史…… 多线程的出现为我(们)打开一片广阔的天地,我也在这方天地摸爬滚打,不断成长!如果说第一单元之前还对Java语法有所了解的话,那么这单元学习多线程则完全是 ...

  10. OO第二单元(电梯)单元总结

    OO第一单元(求导)单元总结 这是我们OO课程的第二个单元,这个单元的主要目的是让我们熟悉理解和掌握多线程的思想和方法.这个单元以电梯为主题,从一开始的最简单的单部傻瓜调度(FAFS)电梯到最后的多部 ...

随机推荐

  1. 13、Spring教程之全部(包括所有章节)

    Spring 教程 1.Spring概述 简介 Spring : 春天 --->给软件行业带来了春天 2002年,Rod Jahnson首次推出了Spring框架雏形interface21框架. ...

  2. (一)SpringBoot启动过程的分析-启动流程概览

    -- 以下内容均基于2.1.8.RELEASE版本 通过粗粒度的分析SpringBoot启动过程中执行的主要操作,可以很容易划分它的大流程,每个流程只关注重要操作为后续深入学习建立一个大纲. 官方示例 ...

  3. OOUnit2Summary

    一.前三次作业内容分析 前言 第二单元的作业以多线程为主题,以电梯调度为背景,分三次要求逐步增加,难度逐步提高.这三次作业,更新了我对于面向对象编程的认知,也进一步提高了我编程和调试的能力. 一下是我 ...

  4. addeventlistener回调函数中的黑科技

    dom.addEventListener('click',callback/obj){},这里的callback除了传递一个函数之外,还可以传递一个属性带有 HandleEvent 方法的对象obj, ...

  5. JDBC_02_JDBC连接数据库 (INSERT INTO)

    JDBC连接数据库 (INSERT INTO) String url="jdbc:mysql://127.0.0.1:3306/employ?useUnicode=true&char ...

  6. pickle json模块

    pickle --- Python 对象序列化 通过pickle模块的序列化操作我们能够将程序中运行的对象信息保存到文件中去,永久存储. 通过pickle模块的反序列化操作,我们能够从文件中创建上一次 ...

  7. Kernighan《UNIX 传奇:历史与回忆》杂感

    Brian W. Kernighan 是一个伟大的技术作家,我买了他写的几乎所有书.他近些年的书我买的是 Kindle 电子版,不占地方. 以下是我手上保存的纸版书: Kernighan 的书大多与别 ...

  8. 逆向工程第005篇:跨越CM4验证机制的鸿沟(下)

    一.前言 本文是逆向分析CM4系列的最后一篇,我会将该游戏的序列号验证机制分析完毕,进而编写出注册码生成器. 二.分析第二个验证循环 延续上一篇文章的内容,来到如下代码处: 图1 上述代码并没有特别需 ...

  9. ZOJ3261并查集逆向处理

    题意:       给你一些点,还有一些边,每个点上都有一个权值,然后有一些询问,分为两种, query a 询问与a直接或者间接想连的点中最大权值的是那个点,输出那个点,如果那个点的权值小于等于a的 ...

  10. Win64 驱动内核编程-29.强制解锁文件

    强制解锁文件 强制解锁因其他进程占用而无法删除的文件. 1.调用 ZwQuerySystemInformation 的 16 功能号来枚举系统里的句柄 2.打开拥有此句柄的进程并把此句柄复制到自己的进 ...