第二单元总结

第二单元总结

本单元的作业围绕着模拟电梯展开,在这个过程中理解并发和多线程是什么,以及如何进行设计多线程的程序,一言以蔽之,多线程的最大的困难在于多个线程如何对共享对象进行不冲突的操作。

由于三次作业有着一定的逻辑关系,故将三次作业整合到一次来描述。

  • 框架设计的进化之路

    • 初识多线程

      ​ 第一单元是从0到1的过程。这一次的作业是最难的,因为这个时候还未理解锁这一概念,那么究竟该如何实现互不干扰的多线程。不过在这个问题之前,不妨先考虑另一个问题,在这次设计中,电梯作为一个关键部分,要能够做到什么程度的智能。

      ​ 这个问题看起来与多线程无关,但其实是避免很多作业错误的关键之处,不同设计对于多线程的优劣放在终章里去描述,这里提出几种框架。

      ​ 1.存在自己的信息队列,存在自己的内部队列

      ​ 2.存在消息队列,不存在内部队列

      ​ 3.不存在任何队列,仅仅是电梯而已,所有行为由上层监管

      ​ 从三种框架来看,电梯的智能程度是越来越低的,但对应的实现难度就更低。而另一个问题是,电梯内部的逻辑和行为要不要分开。比如在逻辑要输出时,是不是也要在同一位置进行行为的sleep,这里的影响并不大,但分开的写法能够省出一点时间,逻辑上的out使得其他电梯可以更快进入状态而无需等待sleep(相当于电梯具有了先验能力)。

      ​ 这里推荐第一种写法+逻辑行为分离的电梯,那么再来考虑如何不冲突的共享对象。面对这个问题,不妨先抛开那些深奥的设计模式,就仅仅思考目的。设计的目的是让电梯能够获得需要的请求,并以此运行(对于架构3并非如此,以推荐架构介绍),那么电梯如何得到请求,由于多个电梯,需要一个统一的输入,而这个统一的输入需要分发给电梯,显然这不是一个线程能处理的。那么需要一个方式去共享。最好的方式当然是利用对象来共享,正因如此引入了对对象上锁的机制,这个机制本质上是,谁拿到了锁谁先进行,谁没拿到锁,谁就等在该处。但有时候,两个线程的条件不一样,可能某个条件没准备好,需要先放出锁,等待条件达成后再被唤醒,这就是wait-notifyall机制做的事。

    • 加装电梯的操作

      ​ 加装电梯这一操作可以理解为再开一个线程,这里就出现了另一个选择,需不需要控制器,很显然如果有一个控制器,那么线程之间的逻辑会更加清晰一点,于是本次作业我就加装了控制器,但由于我的电梯是比较智能的电梯,它只需要拿到消息队列就可以工作,所以加装并不复杂。

    • 换乘的需求

      ​ 换乘本身并不麻烦,真正麻烦的在于怎么保证不同程的任务是符合逻辑关系,而且电梯自己不会出现超时的问题。那么就需要细致地分析我们已经拥有什么,又要实现什么功能,换乘可以理解为分段请求,而请求的下发单位是控制器,但请求的上传单位不仅仅是input,类似于验收的写法可以在控制器里加一个counter(实际实现是一个map),用input的isend和counter来判断结束,同时保证一个passenger类,新增属性真实目的楼座和真实目的楼层,每一个请求都会传回控制器,由控制器来更新其状态并分析是否结束。

      ​ 以上如何实现换乘,其实现看起来很容易,但不少同学都出现了相当多的问题,笔者给出一种可能,他们出现的tle一般都是自由竞争的写法,但自由竞争就意味着不能采用第一种电梯写法,为了更明确认识,这里打表分析。

      名称 电梯内队列数 控制器队列数 楼座队列数字
      自由竞争 1 1(楼座) 1
      人为调度 2 1(电梯的信息队列) 0

      ​ 不难看出,自由竞争天然不可使用第一种电梯写法,因为它突出了竞争,所有电梯去抢乘客,再配上追求平均效果的look算法,这种写法会拥有极高的性能分,但使用它的同学往往会被各种不强的数据hack,而且几乎不可调试,都要读代码解决。

      ​ 那么问题究竟在哪里呢?笔者认为自由竞争是将程序的运行结果完全交给了jvm的线程调度策略,而jvm在线程调度时是不可控的,那么用来结束线程,分配任务的逻辑就要求一种极强的普适性,它需要在各种情况都可行,同时每一个共享对象的notifyall和wait都需要仔细的斟酌,因为他有可能在一些特别的时候出错,那么人为调度的优势在哪里,在于结果的确定性,对于确定的行为,确定的逻辑一定能够保证正确的行为,这就是这类代码不容易被hack的原因,但同时,人为调度的性能完全依赖于算法,需要费力的编写。这里提醒后来者,选择的时候可以追求很强的算法,最差也写一般算法,但绝不要写基准策略,基准策略不一定不强但它一定不会高分,测试时必然会出基准策略难以解决的数据,一般的算法可以选择距离最短+人数最少的写法,效果尚可。

      ​ 最后针对动态分配和静态分配,给出控制器实现动态分配的思路-每次只选定一个节点,还记得之前提到所有的请求都会回到控制器,那么只需要利用bfs算法每次查找一个可达节点即可(这里的bfs的队列是优先队列,也就是其实是迪杰斯特拉算法,由于电梯速度不同,权值也不一样),虽然它不能像动态分配一样,在电梯处理请求时处理新增电梯的路径,但无疑这种写法更为好写,而且不用改动电梯本身,偷懒的写法就以两程为模式,直接遍历查找可达的中间电梯,另外关于bfs是否会超时的问题,理论上不会,两个都是O(n),注意不要出现嵌套的循环。

    • 框架的完整图例

  • 线程之间如何协作

​ 对于上图需要说明,笔者的架构里不需要通知电梯结束,真正结束时,电梯会拿到一个特殊的请求,会自动结束。

  • BUG分析

    • 第一次作业

      在输出时先进后出,在逻辑处理为先出后进,是比较严重的bug会出现超载

    • 第二次作业

    • 第三次作业

  • Hack策略

    • 第一次作业

      测试超重行为

    • 第三次作业

      测试随机测例

  • 比较

    • 第一单元我的测试是基于单元测试进行的,先测每一个小功能合理,再测小功能组合的大功能合理,同时在其中测试特殊的边界,如0值。
    • 第二单元我没有太关注于hack别人,在观察为自由竞争就扔进自动评测中,只有发现运行逻辑有问题才会主动构造测例,但并未发现运行逻辑错误的代码。
    • 比较来看,第二单元的测试才更具挑战性,如何在每一个模块都正确的前提下测出不同模块一起工作的bug,当然这样的bug往往来源于顶层设计忽视了某种情况,但第二单元中,我没有找到太好的方案去测试,当时的一个思路是可以去爬北京地铁数据,进行一个转换。
  • 心得体会

    • 线程安全

      这个角度我的体会是顶层设计决定安全的实现难度,我的写法就没有碰到这类问题。

    • 层次化设计

      我的设计就比较严格遵循了层次化设计每一个模块的功能是单一的,虽然有特别臃肿的类Controller,但从功能来看,它臃肿也合情合理,也是一样的,层次化的设计需要对设计的一个把握,只有心中有设计,才能更好得写出层次化的程序。

posted @ 2022-05-01 21:05  20373kai  阅读(27)  评论(1编辑  收藏  举报