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

设计总述

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

  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. ABP 适用性改造 - 精简 ABP CLI 生成的项目结构

    Overview 不管是公司或者个人都会有不同的开发习惯,通过建立项目模板,既可以使开发人员聚焦于业务功能的开发,也可以在一定程度上统一不同开发人员之间的开发风格.在使用 ABP 框架的过程中,对于 ...

  2. 想要更高效地找到信息,你需要掌握这些搜索技巧 (google or baidu)

    想要更高效地找到信息,你需要掌握这些搜索技巧 (google or baidu) 转载:https://tingtalk.me/search-tips/ 在大型局域网(互联网)的今天,你以为搜索是一门 ...

  3. 【C++】 C++知识点总结

    作者:李春港 出处:https://www.cnblogs.com/lcgbk/p/14643010.html 目录 前言 一.C++常用后缀 二.头文件 1.C++输入输出 2.在C++中使用C的库 ...

  4. 定制开发——GitHub 热点速览 v.21.15

    作者:HelloGitHub-小鱼干 自定义 或者说 定制 是本周 GitHub 热点的最佳写照.比如,lipgloss 这个项目,可以让你自己定义终端样式,五彩斑斓的黑终端来一个.接着,是 Appl ...

  5. python2爬取国家统计局全国省份城市区街道信息

    工作中,再次需要python,发现python用得好 ,真的可以节省很多人力,先说我的需求,需要做一个类似像支付宝添加收货地址时,选择地区的功能,需要详细到街道信息,也就是4级联动,如右图.首先需要的 ...

  6. Leedcode算法专题训练(链表)

    1.发现两个链表的交点 160.两个链表的交集(容易) Leetcode /力扣 public class Solution { public ListNode getIntersectionNode ...

  7. Mediapipe 在RK3399PRO上的初探(一)(编译、运行CPU和GPU Demo, RK OpenglES 填坑,编译bazel)

    PS:要转载请注明出处,本人版权所有. PS: 这个只是基于<我自己>的理解, 如果和你的原则及想法相冲突,请谅解,勿喷. 前置说明   本文作为本人csdn blog的主站的备份.(Bl ...

  8. Spring Authorization Server 全新授权服务器整合使用

    前言 Spring Authorization Server 是 Spring 团队最新开发适配 OAuth 协议的授权服务器项目,旨在替代原有的 Spring Security OAuth 经过半年 ...

  9. 弦图及其在 OI 中的现代应用

    八月份的时候得知要填综评表格,综评表格里面又需要一个研究性学习报告,而我连研究性学习课的老师长啥样都不知道.于是我把两份 OI 笔记拼拼凑凑成了这篇文章充当两份研究性学习报告之一(另一份可能更有趣一些 ...

  10. Python:读写文件(I/O) | 组织文件

    1. I/O 概述  程序与用户交互涉及到程序的输入输出(I/O) 一种类型是字符串,通过input() 和 print() 函数以及数据类型转换类函数如(int()),实现数据的输入输出. 另一种类 ...