• 面向对象设计与构造课程作业 _第二单元总结 _北京航空航天大学计算机学院 2019春季


    面向对象设计与构造课程作业

    第二单元总结

    北京航空航天大学计算机学院 2019春季

    17373468 赵子敬

     

    单元主题:电梯调度

    第一次电梯(2019.04.02)单部多线程傻瓜调度(FAFS)电梯

    第二次电梯(2019.04.07)单部多线程可捎带调度(ALS)电梯

    第三次电梯(2019.04.14)多部多线程智能(SS)调度电梯

    本单元主要希望我们熟悉多线程编程,以及注意线程安全的设计。但是实话实说,虽然完成了三次作业任务,我个人对多线程编程和线程安全设计模式仍然一知半解,并没有深刻理解,三次作业也是在懵懂中完成任务,因此总结和理解有很多不到位之处,还需要通过实践进一步完善。

    一、这三次作业的设计策略

    总的来说,由于个人能力原因,我对多线程的协同和同步控制理解甚少,三次作业都采用了先保证正确性基本要求的大策略,在优化和自动化测试方面都没能做得更多。结合个人能力,我在每次都采用最简单,最能解决基本需求的设计方法。

    第一次作业

    第一次作业是多线程编程的入门,是最简单的傻瓜调度电梯。其实第一次作业由于只有单部电梯且傻瓜调度,完全可以通过单线程实现,但是我采用了课上所介绍的生产者-消费者模式。除主线程外,我开设了指令装载线程作为生产者,用以读入需求指令;开设了电梯线程作为消费者,用以处理指令。另外设计了调度器类,并且采用单例模式构造了唯一的调度器作为托盘,被生产者和消费者共享。

    在同步控制上,同样仿照课上课件的生产者-消费者模式,在托盘的放和取方法上加锁,用信号量实现了生产者和消费者之间的互斥。本次作业由于傻瓜调度,可以每次只向电梯发送一个请求,即托盘只需要保留一个指令请求,其他请求在本请求未处理完之前都等待。最终由主线程启动两个线程。

    第二次作业

    第二次作业虽然也是单部电梯,但捎带需求的出现让单线程一定无法适应。既然有了捎带需求,那么一定需要有一个请求队列来存储所有已经进入电梯调度系统的请求们,而不能让请求阻塞在托盘之外。在刚开始分析需求的时候,我仍然想通过对经典的生产者-消费者模式的拓展来解决这次问题,即我想在调度器,即托盘里设置缓冲区来存放请求,然后等待消费者电梯空闲或有能力捎带的时候去取用调度器里的请求进行处理或捎带。这样的设计模式应该是可以解决第二次作业的任务的,但最终我没有采用这样的方式。这样的方法一来没有办法很好地让电梯处理捎带任务,不容易有层次地实现实时插入捎带任务,另一方面无法让经典生产者-消费者模式适应电梯请求,因为电梯调度的情景,在单电梯情况下请求队列和调度方法无论放在调度器还是放在电梯都可以完成任务,但一旦扩展到多电梯,调度器就需要储存多个队列,实现多部电梯分别的调度,这不太符合多线程编程的设计原则,没有发挥多线程的优势,也没有从架构上实现类方法类功能的单一化。总之,出于向多部电梯扩展的考虑,我最终采用了电梯存储队列的方式。

    在我最终实现的第二次作业中,包括主线程在内我只开设了两个线程,由主线程从标准输入读入请求,由调度器将请求直接发送给电梯,电梯内部维护了一个请求队列,在每一轮请求处理中,电梯的终极任务是处理设定好的主请求,而在处理主请求的路途中根据情况捎带符合要求的请求。捎带的方式我设置为每层查询,即每到达一层楼就检查请求队列中是否有到达该楼层的请求需要出电梯,以及是否有需要捎带的请求可以上电梯。而主线程与电梯线程之间的共享数据仅有电梯的请求队列(因为主线程调用了调度器的分发方法向请求队列写入数据),因此我采用了每次访问请求队列时对请求队列加锁的方法保证线程安全。

    第三次作业

    第三次作业据说达到了所有OO作业的难度巅峰(希望是这样),事实上难度也确实不小,它需要实现三部电梯的调度,同时这三部电梯还有不同的停靠楼层,不同的载客量和不同的速度,这相对于第二次作业而言有了极大的难度飞跃。但是仔细分析我们会发现,难度的增加其实主要在两点,一是真正意义上的多线程需要考虑更多的线程安全问题,二是换乘问题需要电梯和调度器之间的双向交互。至于不同载客量和速度的问题,只会带来优化方面的困难,结合个人能力,我暂时没有把优化过多地纳入考虑,因此只需要着力解决上述两个难点。

    由于第二次作业留下的架构设计有利于向多电梯扩展,我顺着第二次电梯的思路设计下去,只不过这一次我把发送请求单独开线程,而把主线程留出来负责电梯和调度器的初始化。三部电梯,每一部都可以设置不同的停靠楼层、载客量和速度以及名称,并且分别维护自己的请求队列;装载请求的线程负责读入请求,而调度器负责视情况将请求分发给三部电梯中的一部。在两个难点之中我首先考虑的其实是第二个,即换乘问题。由于题目的需求实在是奇怪,三部电梯分别由其不能停靠的楼层,导致有一些特定的请求必须换乘。在不考虑性能的前提下,我首先想到的是“人性化”的调度方法,也就是如有直达则直接发送给直达电梯,如不能直达,则在电梯内部拆分请求,在送达换乘站的同时将换乘请求返送给调度器,而拆分请求的原则是,让不能直达的请求至多换乘一次到达目的楼层。而换成过程中产生了更多的线程安全问题才有了第一个难点。由于调度器只是一个对象,并不是单独的线程,所以电梯向调度器回发换乘请求实际上是在直接访问其他电梯的请求队列,看上去调度器是中转站,实际上它只是形式上的中转站,对于线程安全而言实际上是三个电梯线程在互相访问请求队列。这时需要尤其注意,对任何一个调用请求队列的位置对请求队列加锁。这样虽然牺牲了cpu性能,但是是我这样的架构下唯一保险的保证线程安全的设计办法。

    实际上第三次作业我的设计架构很不好,耦合度很高。为了换乘和回发请求,我让电梯和调度器之间相互调用,并没有遵循单一职能的原则,在线程安全上也没有从架构层面进行优化,可以说是勉强完成任务而已。

    二、基于度量的代码分析

    第一次作业

    代码复杂度

    类图

    第二次作业

    代码复杂度

    类图

    第三次作业

    代码复杂度

    类图

    总体来说,我的设计架构在我自己看来没有有点,缺点主要在于:第二次作业让主线程承担了读请求任务,显得繁琐不清晰,电梯线程的功能实现也比较繁琐,方法调用层次太深不利于debug;第三次作业虽然将读请求单独开线程,各类代码量和逻辑也相对第二次较为均衡,但类与类之间严重耦合,相互调用情况频出,线程安全也颇费脑力。

    三、自己的bug

    在这三次作业中我一共只被发现了一个bug(同质修复后),这个bug也不是线程安全导致的问题,而是一个很重要但是我没有考虑到的小细节。在公测中有四个测试点击中了我的这个bug,显示都是cpu时间超时,但我很明确自己肯定没有用到任何形式的轮询,初步判断是代码中某个意想不到的位置发生了间接轮询导致cpu超时。但是在排查了所有的循环体之后,我确认如果正常运行没有任何一个循环体不经过枷锁等待进行轮询,debug陷入僵局。

    这时,我鼓起勇气拿出长长的公测和互测数据进行人肉分析,在逐条请求分析和调试的过程中我终于发现,在某一个主请求的位置这个主请求并没有被搭载进入电梯,而主请求没有进入电梯时电梯运行状态被我设置为停靠,从而继续检查上下电梯,在处理主请求的循环中进入了死循环——主请求上不去而电梯走不了。再进一步打印相关信息我发现电梯已经满员。也就是说我犯了愚蠢的错误,为了捎带乘客我将电梯塞满后主请求无法上电梯。

    这个bug告诉我,集中注意力考虑周全的点(如线程安全)可能不会出问题,采用保险的策略保障正确性也是很好的编程策略,但思考的全面仍然是最重要的点,要考虑每一个小细节才不至于栽倒在愚蠢的位置。

    四、发现别人的bug

    显然,本单元进入多线程之后hack别人变成了一件奢侈的事情,发现bug的门槛变得很高。拥有自动化测试系统的朋友们成了这场游戏的赢家,和第一单元相比,有无自动化测试的差别不再是赤手空拳和刀剑之间的差别,而是刀剑和枪炮导弹之间的差别。而我就是没有枪炮的那一批人。

    在第一单元,我们可以通过人工分析各种情况来穷举所有可能出现的测试用例情况,只要足够细心,人工找bug甚至胜过了随机生成的数据,因为根据各种情况穷举出的用例更有针对性。但是到了多线程,人工分析变得异常困难。首先我们没有办法实现定点投放,这就让99%以上的测试用例无法通过人工在程序里复现。其次,以人脑的单线程分析多线程程序也变得异常困难,更不要提精准打击程序中的线程安全问题。

    在三次互测中我一共只发现了一次别人的bug,这个bug来源于我在写程序过程中自己出现的逻辑问题,也并不是线程安全问题。我尝试提交了曾经hack自己的用例hack到了别人。

    五、心得体会

    实在惭愧,从线程安全到设计原则,我几乎没有什么理解和体会。线程安全方面我所能做的就是在任何可能出错的地方加锁,而设计原则更是被我抛到脑后。只能说我还得继续努力。

    不过还是可以说说别的心得体会。电梯调度真的很有趣,特别是在考虑优化的问题的时候。吴际老师上课的时候讲,不管是建模还是现实生活,调度问题在遇到大量请求的时候,任何调度方法都显得苍白无力,有时候最简单的调度往往最高效。在高峰期的城市路段,最有效的调度不是基于大数据改变的红绿灯时长,而是在十字路口增派交警,一个方向走完另一个方向走。基于这样的思想,我想我的调度在优化方面也许是成功的。首先我的人性化调度确保乘客进行最少的换乘,毕竟现实生活中谁坐电梯还需要换成呢?其次这种最少换乘的优势在于减少乘客中途等待时间和换乘过程中不同电梯之间的等待时间。朋友圈看到有一些大佬的优化让-3楼去20楼这一简单的请求分了四五次换乘,而仔细想想,优化算法主要在拥堵的时候发挥作用,而最拥堵的时候优化和简单谁更高效还不好说,在最简单的请求上甚至我的直达算法还要由于优化算法。确实优化算法更聪明,但是也许有时候大巧若拙,还真说不准呢。

  • 相关阅读:
    SwiftUI extension Bundle for parse JSON file All In One
    如何清理 MacBook Pro 笔记本电脑外壳上贴纸残胶 All In One
    pure CSS carousel effect All In One
    SwiftUI custom MapAnnotation All In One
    Xcode code folding ribbon All In One
    技术人员副业赚钱之道 All In One
    图解算法数据结构 All In One
    js iterator protocol & next() All In One
    Vue 3 Transition All In One
    Bloom Filter js version All In One
  • 原文地址:https://www.cnblogs.com/zhaozijing1998/p/10764015.html
Copyright © 2020-2023  润新知