电梯单元总结
第二单元电梯作业总结
这篇总结探讨的问题:
- 我自己关于多线程的理解
- 要想做好这次电梯优化(这类问题)需要些什么(极致理论分析)
- 惯例度量分析自己的程序
希望大佬们有错指正,积极分享。
对多线程的理解
在做第一个傻瓜电梯的时候小白的我恶补了多线程相关的各种知识,多亏了此后面两次的作业也没有出现过与线程同步互斥相关的bug。经过了三次作业洗礼(以及OS理论)的亲身体验后,想谈谈自己关于多线程方面的理解。
多线程能帮我们解决什么
多线程就是在帮我们的程序实现并行。实际上,日常生活中串行的事件远远小于无法串行解决的事件,如果我们需要编程实现一个无法简单串行解决的需求,那就只能求助于多线程,当然你也不得不付出处理线程间互斥同步的代价。
优雅地付出代价
那么从串行到并行,我们的代码究竟哪些特性是需要关注的呢?
- 原子性
- 可见性
- 有序性
因为具体展开需要扯到JVM和一些计组的知识,这里就不过多展开,简单的理解可以看多线程理解(一) 三大特性。如果你的多线程程序里对共享变量的操作缺了这三个特性的任何一个,就有可能导致bug。
要让代码满足这几个特性,这三次作业仅采用Synchronized关键字就已经可以完美解决——让一块代码获得原子性,有序性,其中的变量获得可见性。但是不保证后面的作业不会出现例如多个条件的等待等Synchronized关键字无法解决的场景。列出JAVA的几个处理方法。
- ReentrantLock类
- 实现了Lock接口的可重入锁,lock(), unlock()操作与Condition类配合可以实现条件等待。
- Synchronized关键字
- 本质上就是获取JAVA给每个对象(包括类对象)内嵌的一个Lock对象,自带一个条件,通过wait(), notifyAll()使用。
- volatile关键字
- 能够让变量获得可见性以及有序性,对于操作本身自带原子性的对象(各种Atomic类)可以通过一个简单的volatile来实现多线程安全访问。
- CAS(compare-and-swap)等非阻塞算法
- 实际上并没有满足三个特性,其基本思想就是——多线程出现竞争条件是少数情况,我们平时正常读写变量,但是一旦出现了竞争条件,后进行的读写变量操作就会失败同时引发一次重试操作,直至成功。具体实现详见CAS算法
几次作业里的同步互斥控制
这几次作业我采用的都是Synchronized关键字解决同步互斥问题。经过分析找出可能发生共享访问的变量,将对它们的操作进行Synchronized。
但是在互测环节发现其他同学有这么几个问题:
- 用力过猛,很多线程的本地变量的访问也加上synchronized,虽说正确性没有影响,但毕竟使用synchronized机制本身也有开销,可以省的地方还是要尽量省一省
- 偷懒,采用了API提供的一些所谓线程安全的数据结构比如BlockingQueue, ConcurrentQueue后就以为高枕无忧,但是在比如连续if判断的时候,需要一整块代码的原子性,但是数据结构仅提供了各个步骤独立的原子性,实际上是有潜在的出错可能的(但是让他出错的样例太难构造了我也没能hack到他。
电梯的调度算法
深感这次的性能方面的优化之难,难到完全不是一两个算法的选择就能让整体结果有提升的。
究其原因是:输入是带时间的。程序不是全知全能根本没法预知到未来的可能的输入请求是什么,自然任何看起来很智能的算法都可以找到一个输入序列让它变得不那么智能。这似乎就是计算机科学的常态——你完全不知道用户是不是会从杯底喝水。
最近学OS就学到了很多处理未知状态、未知输入的方法。比如进程调度的MLFQ算法,可变大小空闲内存管理的各类算法。所以这里在下一个算法弱只想谈谈方法学= =。
我觉得真正要解决这类型的问题需要几个关键内容:
- Metrics criteria:首先我们要明确这类问题不可能做到面面俱到,相信大家也对这点深有体会。那么我们究竟需要怎么评价算法的优劣呢?这时候就需要度量标准,比如说这三次的电梯作业对性能的要求就只有一个度量标准也就是墙钟时间(从电梯启动到所有请求完成),但实际电梯远不止这一个度量标准,还有比如反应时间(一个请求到来到它被响应经过的时间),最短运行楼层等一系列的指标,我们往往能针对其中一个度量设计出一个最优算法,但设计一个都最优的是不可能的,所以我们要根据重视的度量标准作出取舍。
- Prediction:(此条在完全随机数据面前无效)。在知道怎么判断自己算法的优劣了之后,需要一点面对未知输入的技巧。最常见的就是通过已有的状态预测未来的状态,举个例子:进程调度的时候我们往往希望短小的进程先得到执行,不希望一个重量级进程一直霸占着CPU,但是OS又不可能知道进程究竟需要运行多久才能完成,这时候就可以先用RR算法运行每个进程,占了越多时间片的进程就越可能是重量级进程,这就可以通过这个信息来动态调整他的优先级。
- trade-off:最后就是整合算法的过程,这需要各方面的权衡,属于产出优秀算法最难的一步了。看到讨论区有已经有很多优秀的想法,比如各个算法都跑一遍往err里输出取最优什么的,好狠!
笔者希望能给大家有所启发,抛砖引玉。
度量分析自己程序
分析
这部分我就只分析自己的最后一次的电梯,前两次就不多废话了吧,相信大家都有自己优秀的架构~
从UML图也可以看出来,我整体的架构如下:
- Input:输入线程,负责把请求加入Scheduler中的待处理队列。
- Scheduler:调度线程,不断把queue中的请求取出,经过一定判断后交给对应的电梯
- Elevator:执行请求的电梯线程
- ElevatorAtrri: 储存着电梯人数上限,运行时间等属性
- ElevatorStatus:储存着电梯的请求队列,目的地,乘客信息等状态
这样设计的好处就是耦合度很低,无论是新加电梯属性还是需要调整各处的算法都可以很大程度上复用原有代码。
基于度量来看,各项都很平均,也没有特别需要分析的。
关于BUG
最开始的两项总结已经谈过了。
心得体会
最大体会就是理论知识很重要,特别是算法这方面的,虽然脑袋里有很多好的想法却完全不知道拿什么数据结构和算法来实现他,常常就是把自己卡在实现这一步上,最后妥协采取易实现的算法,体验比较差。