301
社区成员
发帖
与我相关
我的任务
分享在第一次作业发布时,我对于多线程的理解还不到位,对架构的设计毫无头绪。但在当周的实验课上,实验课的考试代码给了我很大的启发,让我对多线程处理任务有了初步的理解。在后续的这几次作业中,我都延用了实验课的代码架构,虽然并不一定是最优的,但一定是条理清晰、思路合理的。另一方面,这也说明当涉及到难度较大的新知识时,实验课还是很重要的。
线程
如前言说,本次作业基本完全复制了实验课代码的架构。即设计三个线程类:
Input线程负责输入请求(即乘客信息Request),Schedule线程负责将乘客分配给对应的电梯,Elevator线程负责将乘客送达目的地。
共享对象
经过分析可知,在该架构下的共享对象有Input线程和Schedule线程共享的总RequestQueu队列;同时每个电梯线程还会和Schedule线程共享每个电梯的Queue队列。所以在后续的代码实现中,对于这些共享对象的操作要注意及时加锁,以防出现线程安全问题。
一系列wait问题
关于线程wait的问题是很关键的。如果不及时wait,容易导致代码空跑,造成CTLE;如果wait不能及时唤醒,会造成该线程永远wait下去无法结束;如果wait的判断条件有误,会导致线程在不该wait的时候wait,以致无法正常完成请求……
所以在设计之前,我们必须先明确需要wait的情况:
并且我们需要对wait的判断条件进行特殊的分析,防止判断wait的条件由于线程不安全的问题而导致读写之类的问题,从而产生上述可能导致的问题。


调度器是整个电梯任务中非常关键的一环,在这一次作业中可能体现的还不够明显,但是在后两次作业中就能体会到这一点。由于本次作业指定了分配的电梯,所以调度器直接进行分配即可。即便如此,Schedule线程和其他线程的交互问题仍然值得我们讨论。
调度器结束:Schedule和Input的交互
调度器的结束是以总的waitQueue属性作为判断的。首先,Input线程结束,将waitQueue置end,表示不会再有新的请求进入总的候乘表;如果这之后某一时刻检测到waitQueue为空,则可以结束Schedule线程。如果想设置同步块的话可以在判断条件之外设置,但是我个人认为这个地方可以不设置同步块。如果不设置同步块的话,唯一会产生的意外是当刚刚判断完是否return且判断不return的下一时刻,waitQueue发生变化且变化完之后符合return的条件。但是这样不会出现安全问题,顶多会空跑一遍代码(因为也不会进入别的分支)。

调度器setEnd:Schedule和Elevator的交互
当调度器结束的时候,会把每个电梯的候乘表setEnd,表示电梯候乘表不会再接受来自调度器的请求。这是十分重要的一步,如果这一步出错就有可能导致电梯的end属性出问题,从而电梯无法正常return。
分配给对应电梯即可。
线程
本次作业和上一次作业的线程类基本没有发生变化:
共享对象
与上一次作业相比,共享对象也没有发生较大变化。唯一不同的地方在于,与上一次作业相比,每个电梯又多了resetQueue和everyQueue,这也需要和调度类线程共享起来。
一系列wait问题
与上一次作业相比,这次作业多了一个需要wait的情况:
为了解决这个新增的问题,在受到同学的启发之后,我设置了一个 “另类” 的容器,这个容器十分 “务实”:只有当请求被真正得到处置之后才会从这个容器中离开。对于乘客请求,只有当乘客是在自己的目标楼层下电梯的时候才会被移除;对于reset请求来说,只有当reset请求进行之后才会从这个容器中删除。这样的话只需要简单地将这个容器的属性加入判断条件即可。


调度器结束:Schedule和Input的交互
和上一次作业完全一致
调度器setEnd:Schedule和Elevator的交互
和上一次作业基本一致,只不过判断条件需要发生变化,只需要加入对上述容器的判断即可。
本次调度策略我选择了调参法,即根据电梯的属性来计算对于每个请求电梯能够得到的分数,得分高者具有接受这个请求的资格。
但是有两个问题十分棘手,也就是这两个问题让我觉得调度策略的完美化是不存在的,研讨课上的random大法更验证了我的想法
同步块设置的问题:由于每个电梯都是线程,当计算完第一个电梯的得分之后,在计算第二第三个电梯得分的时候,电梯一的状态是会改变的,也就是如果不设置锁的话,计算得到的分数永远会有误差。但是如果设置锁的话就更显得搞笑,这表明每来一个需求需要计算分数的时候,所以电梯必须“立正”,不许移动,这实在是太低效率了。
设置参数的问题:无法精准设置参数,应该是能明确知道参数和效率的关系的,但是需要复杂精确的计算。比如哪些标准是最重要的?如果超载的话一个人对应多少分数?这些都是很精细的问题,很难有完全准确的回答。
综上,即使是调参法,也只能粗略进行估计,在研讨课上很多random和模6拿的分数还是蛮高的。而且写调参法的时间代价太大,不如简单高效的随机和模6.
线程
本次作业新加入了线程DCElevator:
Input线程负责输入请求(即乘客信息Request),Schedule线程负责将乘客分配给对应的电梯,Elevator线程负责将乘客送达目的地,DCElevator线程即双轿厢电梯。
共享对象
和上次作业基本一致。
一系列wait问题
和上次作业相比又增加了wait的情况,但是wait的条件是不用改变的。所以不再赘述。


调度器的交互没有太多改变
模6法。
关于双轿厢电梯不碰撞问题,我的主要思想就是:当电梯运行到敏感楼层的时候,要在移动之前进行think,并且改过程要设置同步块,即两电梯一起进行think and move,这样就可以避免双轿厢的相撞。
在这个过程中有一个关键的问题,那就是同步块的设置范围。设置同步块的原因是避免当进入分支之后判断条件中的属性又发生变化,从而导致两个电梯都开始移动。但是我个人认为,同步块的设置范围也不能过大,否则会降低效率。例如在本次作业设计中,我的同步块设置如下:

其中,我没有在最外层的if分支设置同步块,而是在if内层设置了同步块。如果在if外设置了同步块,那么电梯线程每圈run都要在这个地方停下来,会降低效率。如果我这样设置的话,不仅不会降低效率,还能够解决问题:如果判断发现当前我处于敏感楼层,我就需要用锁来限制接下来ta和ta对应的双轿厢电梯的移动;如果压根就不处于敏感楼层,那么我不需要担心,可以放肆移动。
多线程的bug层出不穷,基本所有的bug我都出现过。
CTLE
这类的bug基本是由于没有wait而造成的轮询。我出现这类bug的原因是在一开始没有完全搞清楚wait的本质,理解了之后改起来就比较简单。
RTLE
这类bug是最恶心的,基本上由两种原因造成,一方面是调度策略太拉胯了,一方面就是某些线程醒不了。线程醒不了是最常见的情况。在我编写代码的过程中,曾经出现过不该进入wait进了wait或者进了wait发现没有notify。其中最明显的就是对于第六次作业的reset请求,如果返回人了还好,返回的请求可以通过add操作进行notify;但如果没有请求返回的话,我的Schedule就一直陷入等待的状态而无法唤醒。
我使用的基本都是打印输出法。例如在wait语句上下打印输出,看是否是进入wait而无法唤醒,或者压根没有进入wait。
早就听说过电梯月的威力,不过当真正接触到这单元作业的时候,才真正感受到电梯这一单元到底难在哪。
首先是对于新知识锁和同步块的理解。只有真正理解锁以及同步块的作用才能够更好地去理解线程的工作原理,更好地处理线程安全问题。
其次是对架构的处理和迭代。多线程问题需要一个相对合理和良好的架构,实验课代码为我们提供了基本上是一个范例,告诉我们如何能够更好地对多线程任务进行处理:输入、调度、运送……这些不仅符合代码的需求,其实也和我们现实中的情境是相似的。同时,我们还必须全面了解线程之间的交互,每个线程不是独立存在,和其他线程完全没有干系的,而是存在微妙交互的。如果处理不好线程之间的关系,将会出现很多离谱的错误。
最后就是坚持,bug总会被消除,坚持,坚持,坚持!