442
社区成员




这个标题有迎合作业格式的意味,本人更愿意理解为如何在多线程编程进行同步控制。第二单元总共使用了四种方法来控制同步,分别是同步块、阻塞队列、信号量、类似 CAS 的操作。实际上,同步块和阻塞队列的也需要锁来实现。
同步块
对于本单元的同步控制,同步块的作用主要体现在保护共享变量,避免多线程引发的数据竞争。对于同步块的大小,过大可能会使多个线程不要的等待,过小会造成逻辑上的缺陷。需要注意的是同步块内部不要涉及 Thread.sleep()
的操作,这会造成线程拿着锁进行等待,严重阻碍其他线程的运行。同步块上锁的变量自然需要在同步块中使用,尽量减少嵌套多层同步块的使用,如果出现过多层的嵌套首先应该考虑是否是架构的缺陷,然后考虑一次性获取多个资源,避免潜在的死锁问题。
阻塞队列
对于乘客的传递,为控制器设置一个总等待队列和为每个电梯设置一个单独的等待队列,这样的机制保证了在系统没有停止运行时的任何时刻向控制器或者电梯新增乘客,不需要额外的同步操作来通知接收方。接收方只需要从队首取出元素,没有元素则等待,这些操作都已经由 BlockingQueue
接口定义好,无需我们手动实现,一定程度上减少了 bug 的发生。
信号量
S=1
时即为互斥锁, S>1
时能实现更多的同步操作。第七次作业新增的服务中、只接人的电梯就是为信号量量身定制的需求。为每层分配两种信号量,初值分别为 Mx
和 Nx
,在开门前电梯就可以根据电梯内部维护的乘客队列得知是否有乘客下电梯,获取相应的信号量即可。对于特殊的情况,比如在电梯开关门状态切换时发生 maintain
等操作,下电梯的人肯定是增加的,也就是说服务中的电梯可能变成服务中、只接人的电梯,而服务中、只接人的电梯不可能变成服务中的电梯,前者只需要保证开关门前后获取和释放的资源一致,后者是最需要担心的情况但是并不会出现。
CAS
在本人的设计中,控制器的调度一个人需要获取电梯组的信息,需要获取若将乘客分别分配给每个电梯后该电梯的最终运行时间,如果都放在同一个同步块中,难免会导致同步块过大的情况,实际上,计算过程是很迅速的,而维护、加电梯的指令数量很少(就算全是也就几百条),于是乎,控制器在获取到乘客后,记录当前电梯组状态,计算将该乘客分配给哪个电梯,在向该电梯分配乘客时判断电梯组状态是否改变,即判断是否有增减电梯的操作,若有则重试,没有则向该电梯分配该乘客。
Elevator
的静态方法视作电梯组的方法,其他方法视作每个电梯各自的方法,通过 mainTainRequest
和 elevatorRequest
向电梯组发送命令,电梯组再通知控制器重新规划静态策略 remap
。 对于前两条,问题简化为将已分配的乘客送至目的地。电梯使用状态机模型,根据已规划的路径运行。根据已分配的乘客规划路径,产生一个乘客队列,电梯只需向队首乘客的目的地运行,到达目的地后开门进出人即可。这样的运行策略相当简单,且每次运行时的行为只跟当前状态和当前乘客队列有关,与上次状态以及上次状态的乘客队列都没有关系,可以很方便的在切换状态时修改乘客队列。
对于前三条,即第五次作业的要求,新分配的乘客加入一个线程安全的队列 waitQueue
中, 电梯在每次状态切换后判断有无新分配的乘客,有则重新规划路径,生成一个新的乘客队列(贪心算法)。在不超过载客量的情况下前往与当前所在层最近的乘客的目的地。
对于四、五条,即第六次作业要求,电梯在每次状态切换后判断是否 maintain
, 若有则将所有电梯外的乘客直接丢回控制器中的总等待队列, 判断是否处于开门状态后, 把电梯中的乘客丢出去。一旦有 add elevator
指令, 则将所有现存电梯的 reorder
标志设置为 true, 电梯在每次状态切换后判断是否 reorder
, 若有则将所有电梯外的乘客直接丢回控制器中的总请求队列。避免新电梯饥饿的情况。
对于第六条,即第七次作业要求,由于知道当前层是否有乘客出电梯, 直接使用信号量实现。特殊情况已经在信号量部分说明,不再赘述。
对于更多的需求,在每次状态切换时可以进行判断。
从总请求队列中取出一个乘客并选择一个电梯分配
处理计算出分配的电梯后电梯没了的情况
乘客路径规划
i
和终点 j
, 乘客的需要前往的楼层为 map[i][j]
i
和终点 j
计算需要前往的楼层 k
, 此时若将乘客的终点视为 k
, 则分配该乘客的分配策略其实与前两次作业没有变化(只在能到达 i
, k
层的电梯中计算)95.4028
98.9533
、99.0926
虽然贪心看起来是个很怪异的算法,但是实际运用上不不输其他算法。可以考虑这样一个情形,对于给定的乘客,让一个电梯将他们送到目的地,ALS
和 LOOK
的原则都是回朝一个方向走,捎带乘客,然后到达后再转向继续接送乘客,而贪心其实在开始时会有小范围的波动,即上下小范围运动,但是当这个范围没有目的地后,电梯有很大的趋势向着同一个方向运动,之后的行为与前两个算法相似。对于某些情况,反而具有一定的灵活性。此外,贪心计算出乘客序列后可以得知该电梯还需要的运行时间,这为控制器提供了调度的依据。
使用贪心不可避免的会导致乘客饥饿的等待的情况出现,控制器的调度在一定程度上缓解了这种情况,因为控制器的贪心策略要求总电梯运行时间最短,尽量让乘客的需求得到响应。在 maintain
和 add elevator
的设计上,向控制器的总等待队列插入的乘客位于队首,而从输入的插入的的乘客位于队尾,优先分配先到乘客。
根本没管
Floor
枚举类0
、1
、0
程序基本没有出现过死锁的情况,由于同步块基本没有嵌套,唯一可能导致死锁的地方只有结束指令没有及时被响应,导致线程等待,不结束。在线程死锁后使用 jconsole
连接对应线程可以看到详细信息,从而进行调试。
输出和状态改变的先后顺序可能导致输出顺序的改变,虽然概率相当小,在本地测试的几万组数据中出现过一例,但是依然应该引起重视,注意先输出再改变状态,避免状态修改后输出前产生其他输出导致的问题。
虽然此前接触过 GPU 上的并行编程,但是要实现本单元的需求还是需要相当多的时间,多线程情景下有很多同步的策略,在一些地方可能还有更好的实现方式。本单元没有被 hack 过一次,得益于为本单元编写的多线程评测机,使用十几万的随机数据来测试程序的正确性,在很大程度上保证了最后提交程序的正确性。同时使用的贪心策略也取得了相当不错的成绩。在迭代方面,也免去了重构的麻烦,结构可以很好的适应每次新增需求甚至评测机的架构也能很好的迭代。