北航计算机学院面向对象(2023 第二单元)
简介
本文将以笔者学习过程中的思考感悟为基础,对2023北航计算机学院面向对象课程第二单元(作业5~7)的架构搭建和程序设计思路做简明的描述;如有不同见解,欢迎学习交流。
一、同步与锁
在该节,将对三次作业中线程互斥和安全问题进行系统性分析。
1.共享资源的确定
在多线程开发的工程中,多个线程常常需要访问同一个区域的资源,这表明我们需要确定多个线程可以访问的类、变量、方法等来选取共享资源。
在我们的作业中:
- 输入线程将输入置于请求队列中
- 电梯线程从队列中获取乘客
- 需要换乘时电梯线程将乘客写入请求队列中
- 调度模块检索请求队列为各电梯提供决策
可见该队列及其行为即为该系统的共享资源,我们将其命名为请求表(RequestTable),并将其封装成一个专门的类便于后续线程互斥的操作。
2.线程互斥的判定
线程互斥的“锁程度”(笔者杜撰,即被资源加锁的代码块长度)是个微妙的话题:
过高的锁程度为编程人员带来很强的安全感,但多线程程序的执行效率可能受到影响,甚至趋近于单线程。
在未保证线程安全的情况下过低的锁程度可能导致线程对同一资源的访问冲突,导致线程安全问题。
故决定何处的代码块应该被加锁,不加不必要的线程锁是十分重要的。
根据Bernstein条件:
两个线程在某区域可并发执行,当且仅当对于该区域:
- R(S1) & W(S2) = ∅
- R(S2) & W(S1) = ∅
- W(S1) & W(S2) = ∅
其中 R 表示读行为,W 表示写行为。
故当资源不可被多个线程访问(线程不可在该区域并行,即需要加锁),以上三个条件至少有一个无法满足。
基于此,在三次作业的代码中,与 RequestTable 相关的读写代码块都应该被加锁;这样处理可以保证共享资源不被同时读写,保证了共享资源的线程互斥和安全。
二、线程协作和调度
在本节中,将介绍三次作业的架构和线程协作模式,分析调度策略的更迭。
1.代码架构
以下是本实验的UML架构,三次作业差别不大,故取第三次作业的架构进行分析:
系统架构分为6个大类,简要功能如下:
类名称 | 功能 |
---|---|
MainClass | 主类,接受输入包的输入,负责开启电梯线程 |
Elevator | 电梯类,是唯一的继承Thread的类,包含电梯的各个行为方法,和状态函数 |
Person | 请求单元类,存放并维护请求单元 |
RequestTable | 请求表类,该类为各个电梯线程共享,提供请求表的信息和维护方法 |
Dispatch | 决策类,根据电梯和请求表的状态为电梯提供下一步行为的决策 |
ArrangePath | 寻路类,仅在第三次作业中使用,解决人员的必要的换乘需求 |
以下是三次作业的架构变化分析:
作业序号 | 功能更迭 |
---|---|
1 | 通过电梯线程的模拟,实现人员在楼层间的一次传输 |
2 | 增加电梯的增减功能,只需维护一个Elevator类数组即可,但线程终止条件需要进行修改 |
3 | 增加换乘、电梯楼层限制、同时服务限制;为此,添加寻路类和服务数、服务楼层的特判 |
2.调度和决策
在三次作业中,均采用自由竞争的策略;前两次作业电梯类间无约束,最后一次作业为此策略添加了控制和规划的元素。
(1)纯粹自由竞争
该策略十分容易实现,且可实现人员输送的速度最大化,但对于耗电量的处理在人员流量下的情况下可能效果不佳。
该策略具体如下:
- 电梯内为空时,向着 RequestTable 内有请求的方向移动,否则原地待命。
- 电梯内有人时,到相应楼层,相应楼层请求若与电梯行进方向相同,则开门捎带。
- 电梯在 1层 和 顶层 自动切换运行方向。
可见该策略逻辑简单,在做好线程安全和同步的前提下,电梯线程间互不干扰,速度性能和安全性能较佳,但耗电量不可控。
(2)规划自由竞争
在第三次作业中,电梯的达到楼层和同时服务数限制,自由竞争策略在这种条件下需要进行改进,具体改进思路如下:
- 引入 ArrangePath 类,使用 Djstra 算法进行寻路,具体处理如下:
- 将电梯的可达楼层掩码转化为楼层可达矩阵
- 将各个电梯的可达性矩阵加和得到总图
- 对总图应用 Djstra 算法,依据人员始末楼层规划最短换乘路径
- 维护或添加电梯时只需使用总图减去或加上可达矩阵即可。
- 在每次维护、增加电梯时更新总图和 RequestTable 中的人员路径;在每次人出电梯时更新出电梯需换乘的人员路径。
- 在电梯到某一楼层时,依次检索电梯外的人员,若其下一个到达楼层该电梯可达且电梯容量未达到上限,则开门接人。
- 在电梯到某一楼层时,依次检索电梯内的人员,若人员当前需到达楼层为该层,则开门放人,若该人员的终止楼层为该楼,则释放该请求,否则将该人员的起点楼层和下一到达楼层修改,重新放入RequestTable 中。
- 在共享的空间中添加“正服务”和“只开门”数组记录楼层的服务信息,当电梯在某楼层确需开门中先结合该数组进行判断,再决定是否开门。
该策略中,保留了自由竞争的成分,但使用了共享资源作为限制条件,并针对人员使用路径规划,在保证整体架构不变的前提下实现了功能的迭代。
该策略中,电梯线程间不直接相互影响,而是通过共享资源进行间接限制,在不引入调度线程的前提下,实现了电梯准确性、速度和耗电量的权衡。
(3)线程时序分析
以下是主线程和电梯线程的时序图:
可见该系统的时序调用十分简单,电梯线程仅仅由主线程创建,线程间的关系较为单一,便于代码的编写和分块调试。
3.易变分析
在三次作业中,迭代开发的需求较为显著,可分为稳定和易变两个方面:
- 稳定部分
- 电梯的行为:移动、等待、开关门、结束运行
- 人员的起始终止楼层信息
- RequestTable 的结构
- 易变部分
- 电梯的容量、可达性、速度等参数
- 楼层的同时服务限制
- 电梯的终止条件限制
- 电梯数量的变化
其中稳定性部分多是对象的基本行为属性,这也在意料之中,因为现实中电梯可执行的动作也基本上在第一次作业就被涵盖了。
易变的是电梯的性质和运行规则,为适用与不同的应用场景,这些参数是可以被频繁修改的。
三、多线程Debug策略
对于多线程程序,debug 无法使用单步调试工具,故在 debug 和 hack 过程中,需要采取一些特殊的策略;虽然这一单元笔者在强测和互测中均为出现 bug,也为发现 bug,但在课下的调试过程中,笔者收获了一些调试技巧和感悟。
1.Debug 策略
- CPU_Time_Limited_Exited
出现该 bug 的原因一般是代码中产生了轮询(频繁地访问或修改一个量),定位该 bug 可选择在含有循环的结构中打印一些信息,若运行时某一信息出现次数过多即该处很可能发生了轮询。
例如笔者第三次作业的课下,由于决策类的设计笔误,电梯在本该停下的时候发生了不间断的转向,导致方向信号量杯反复修改,通过打印电梯的行为可以直接定位 bug。 - Running_Time_Limited_Exited
出现该 bug 的原因可能是调度策略问题(完成得太慢),但这种情况可能性很小;更多的是电梯线程未正常结束,大多是人员未完全送到电梯就都停下了。
避免该 bug 的机制也比较简单,可以在输入结束后维护一个信号,在电梯运行时维护剩余未完成请求的变量,当结束信号产生、变量归零时即可结束进程。 - Runtime Error
该 bug 在本次作业中大多由于遍历结构时对结构进行修改产生(遍历和修改可能是两个不同的线程),故在同一线程中注意缓冲区的构建,在线程间协调好安全问题即可。 - Wrong Anwser
该 bug 的原因十分多样,可能有人员送错楼层,行为时间间隔有误,电梯到达不可到达的楼层等,但评测机的出现使这一类 bug 的定位解决较为简单。
2.Hack策略
在三次 hack 中,笔者未能命中(可能是大伙都用评测机跑了),但还是有一些数据构造策略:
- 短时间,大流量,长跨度数据的投放
- 边界数据(增减电梯在边界层)
- 极端条件(废除尽可能多的电梯)
- 换乘构造(构造可能需要多次换乘的数据)
四、写在后面
在本单元的练习中,笔者对多线程编程有了大概的理解和掌握,并初步进行应用。
对于线程安全问题,笔者采用同步代码块的方式,将主类中声明的 RequestTable 对象作为共享对象,实现了线程访问的互斥;在线程通信方面,由于本架构线程间耦合性低,故采用 sleep() 方法代替 wait-notify 机制进行必要的等待,这种方式降低了进程间通信的性能消耗,但线程间的关系不易看出,实在算不上好的策略。
对于调度器的编写,笔者三次作业坚持延续自由调度的核心,以耗电量的增加换取代码的简洁和输送速度的提高,虽说任何调度策略都需要在这两个方面做权衡,但自由调度的策略还是太偏向于速度,忽略耗电量了(太极端,易被样例针对);虽然最后的性能得分都在95左右,但要将该系统部署在现实中还需要调度策略的优化。
总之,笔者认为自身对安全性和代码简洁性的考虑有些过头,策略选取和类构造都尽可能简单,这是之后的作业中需要权衡改进的。