OO第二单元总结与分析

李从容-22373301 学生 2024-04-20 18:10:31

OO第二单元分析与总结

概览

OO第二单元重在多线程。通过“电梯系统”这个背景来构造多线程程序。

其中,三次作业都是什么要求呢?

  • 第一次

    • 乘客发出请求,并指定了要乘坐的电梯。

    • 这次的就是把架构搭起来,从零开始先建立框架

  • 第二次

    • 这次不再指定电梯:所以需要增加调度策略

    • 新增RESET指令:需要在流水线中传递这条RESET以实现电梯重置

  • 第三次

    • RESET可以将电梯重置为双轿厢电梯

    • 这个需要新增双轿厢电梯,并对管理线程的数据结构进行修改以实现撤销并删除、新建并加入双轿厢线程

如何实现线程安全(锁与同步)

第五次中,我主要使用了synchronized 方法,实现了两个线程安全类:requestMap and requestQueue

所有线程的运行都是对自己或者这两个类操作,也只有这两个类的对象是共享资源。所以,很好地保证了安全性。

但是,第6次作业中,新增了RESET。我没有将RESET作为和其他指令一样地流水下去,而是选择接受到之后直接传给电梯线程。而由于RESET需要踢人,所以难以避免地需要共享许多许多对象。 这时,容易产生一些问题。

另外,最容易出问题的就是分发器如何结束所有电梯线程和自己?这又涉及到线程协作的问题。这里,我采用synchronize方法 + wait + notifyAll。

synchronized代码块虽然能实现精确控制,但是确实比较难写而且比较容易错。synchronized方法则要方便的多:另外,wait的目的是为了让线程休息,免得浪费资源(比如本次的轮询)。但是,wait的线程很容易不被唤醒!这也就是大多RTLE的原因。这里需要分析好什么情况下会wait,在这种情况下wait了,都有没有notify能唤醒这个线程。这个靠评测机很难保证覆盖所有情况,个人感觉需要自己审查代码,进行逻辑分析。(jby的“红蓝法”提供了一个很好的解决死锁问题的方法)

调度器设计

调度器如何与线程交互

在我的设计中,调度器为单独一个线程,作为中间桥梁的作用,上头通过RequestQueue与Input交互,下头通过RequestMaps与电梯线程交互。当然,在引入了RESET后,还会产生电梯线程与调度器通过RequestQueue的交互。

图示如下:

 

线程关系

 

调度器使用的调度策略

没有选择影子电梯

先是%6,之后发现在%6前可以来一个“能带就带”来筛选一个电梯。(其实这个类似于写一个估值函数)当然,也可以选择不用%6,使用random。%6会更稳定,不过似乎random不少时候表现会更优秀?

架构

架构设计

 

首先,老师在课上提到了生产者消费者模式,看了之后的确和这次作业很匹配。这里,其实第一次作业直接给每个电梯来一个RequestMap作为中间的channel就可以了。如果考虑之后的拓展,可以来一个总的RequestQueue,再给每个电梯来一个RequestMap。

然后,从各个线程上考虑。(用的是RequestQueue + RequestMap的二级结构) 刚开始我是直接一个DispatchThread负责输入+分发。但是之后改成了Input + Dispatch。这样的主要原因是进行了两层隔离后更加清晰,并且,这样在拓展时就可以实现在Dispatch中实现分发的策略。

UML-class

 

线程协作关系

Main创建所有线程后,使命就结束了;

Input输入所有Request后,使命也结束了;

最终留下的是Dispatch和Elevator:Dispatch取得request并分发给EleThread,EleThread执行任务,并且有可能把request还给Dispatch再分发(RESET时)。EleThread还有可能自己创建新的EleThread线程(DoubleRESET)

这两类线程,需要到最后再结束。

sequence

 

三次作业的变与不变

变:要求在变,其实也就是电梯功能一直在变:先是固定接人,然后随便接,然后加入RESET,之后甚至有DoubleReset。

但是,变的是功能,不变的是架构。

我的架构在三次作业中没有任何变化,而且完全可以适用。原因就是,电梯系统,或者说这就是一个生产者消费者模型,这样的流水线架构在绝大多数情况下是没有问题的。

当然,别的也有一些没变,比如电梯的LOOK策略等等。但是,核心当然是架构。

如何避免轿厢相撞

关于如何避免相撞,有两种思路。

思路一

最开始的想法是:直接规定A电梯可以进入transFloor,B不允许进入(因为我的设计中,乘客并不一定非要双轿厢从头送到尾,双轿厢只是扮演了功能稍弱一些的普通电梯)

但是,如果6台全部RESET,且transFloor相同,那么该电梯系统就无法完成跨越换乘层的请求

在舍友fxk的点拨下,意识到可以设置一个DC_RESET_cnt,第一次让A不许进入,第二次就换成不让B进入换乘层……这样就一定能满足要求

思路二

在讨论区学习了姜涵章的方法后感觉十分巧妙。

实现一个类Flag,一个线程安全类,用于标记一组双轿厢的换乘层有没有被occupy。

不过讨论区Toby Shi的回答又点醒了我:确实只需要一个锁就行了,并不需要自己搞一个类(

如何在多线程中debug

首先,多线程程序不可避免的还是会写出各种单线程bug。对于这种情况,加断点就很轻松可以de出来。但是这里,以idea为例,右键选择断点后,需要把断点改成thread模式,这样可以阻塞其它所有线程实现单步运行。

对于多线程容易出的bug,个人感觉最多的就是线程不结束。如果有测试数据的话,就可以逐层定位快速找到原因:先在线程的run方法开始和结尾加print语句,首先定位是哪个线程没结束;下一步定位其中的执行方向是什么;最后看是什么情况导致没结束。 如果没有测试数据,我感觉很麻烦,网上有介绍死锁检查工具,似乎不太好用(我不太会)。这时似乎只能选择读代码(不是顺序读,而应该是横向、遍历所有情况的去执行)

关于ConcurrentModification

我的board是一个HashMap<Integer, HashSet<>> 在送人出去的时候,不会出现ConcurrentModification,因为这时候没有人会进来(这个电梯就这一个线程)(只有这个电梯线程会操作它的board)

我的requestMap的get方法直接把那个HashSet拿了出来,这时候可能会再进人!所以在openIn遍历HashSet看有没有人要进来的时候出现了问题:遍历的时候,HashMap是不能改的,但是这个时候DispatchThread可能会再往HashSet里面装request,导致ConcurrentModification。

考虑到对于openIn只有这种情况:openIn正在遍历,DispatchThread正在装。所以,感觉只要openIn里面改成Iterator就可以了,这样效率还可以提高。 不对!并不行!迭代器只能remove,不可以由别的线程对HashSet添加!!!还是会ConcurrentModification!

心得体会

这次的电梯作业,前两次都还是很清晰的。但是第三次由于设计较少,思路混乱,导致出现了很多问题:逻辑错误,线程安全问题,多线程bug……

多线程的确更加需要“think twice,code once”。动手之前,设计好架构,尤其是考虑好共享资源与可能出现的线程问题。虽然即使这样也必然会出问题,但是思路清晰至少有利于de多线程的bug。

 
 
 
...全文
58 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

301

社区成员

发帖
与我相关
我的任务
社区描述
2023年北航面向对象设计与构造
学习 高校
社区管理员
  • YannaZhang
  • CajZella
  • C_ecelia
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧