305
社区成员
发帖
与我相关
我的任务
分享本次 Unit2 是针对多线程的安全与通信等问题的综合考察,难度很高。在三次迭代中,深刻体会多线程锁的使用。
本次作业中,我的同步块设置都比较粗,很容易导致死锁问题,这也是我后面迭代出现的主要bug。主要的同步块设置在了总请求队列 RequestQuque 和电梯处理队列 ProcessingQueue 中的新增请求和取出请求,和电梯线程的状态获取。设计架构主要仿照第二单元的训练题目,虽然在请求处理上并不是队列的形式,但一定程度上依旧先进先出。
除此之外,在电梯线程类 ElevatorThread 的下车方法需要获取锁,用于操作电梯内部乘客的容器。(但是似乎可以不加锁)
private synchronized void dealOut() {
if (!personIn.isEmpty()) {
Iterator<PersonRequest> iterator = personIn.iterator();
while (iterator.hasNext()) { // 有没有到的下
PersonRequest request = iterator.next();
if (Statics.floorToInt(request.getToFloor()) == currentFloor) {
openDoor();
getOut(request, iterator);
}
}
}
}
以及电梯捎带请求的获取方法(在 ProcessingQueue 类中加锁),用于保护两个容器的删除操作。
public synchronized ArrayList<PersonRequest> getByTheWay(
int currentFloor, boolean isUp, int curWeight) {
Iterator<PersonRequest> iterator = floorIndex.get(currentFloor).iterator();
ArrayList<PersonRequest> byTheWay = new ArrayList<>();
int currentWeight = curWeight;
while (iterator.hasNext()) {
PersonRequest request = iterator.next();
if (isByTheWay(request, isUp)) {
if (currentWeight + request.getWeight() <= 400) {
byTheWay.add(request);
iterator.remove();
currentWeight += request.getWeight();
}
}
}
if (!byTheWay.isEmpty()) {
queue.removeAll(byTheWay);
}
return byTheWay;
}
其实,上述的锁操作完全可以替换为对类内部某一个容器单独的加锁,这里对整个对象加锁,粒度过粗,容易死锁。
private final HashMap<Integer, ProcessingQueue> queueMap;
private final HashMap<Integer, ElevatorThread> elevatorThreads;
通过在创建调度器时,加入两个容器,用于访问每台电梯的等待队列,以及每台电梯本身。使用电梯类和队列类自身的get方法来获取线程信息。
...
queueMap.get(id).offer(personRequest); // 分配请求
...
for (HashMap.Entry<Integer, ElevatorThread> entry: elevatorThreads.entrySet()) { // 遍历电梯线程
ElevatorThread elevatorThread = entry.getValue();
...
}
// DispatchThread
private void dispatch(Request request) {
if (request instanceof PersonRequest) {
PersonRequest personRequest = (PersonRequest) request;
for (int i = 0; i < 6; i++) {
// 获取电梯状态
if ( /* 主电梯 NORMAL 且能接 */) {
...
} else if ( /* 主电梯 DOUBLE/WAIT */ ){
if ( /* 主电梯能接 */ ){
...
} else if ( /* 备用电梯 DOUBLE/WAIT 且能接 */ ) {
...
}
}
}
try {
sleep(100);
} catch (InterruptedException e) {
//
}
requestQueue.offerBack(personRequest);
} else if ( /* 特殊请求 */ ) {
...
}
在调度策略上,没有采用什么较好的分配逻辑,使用了简单的 1 到 6 的循环分配。但是在判断电梯能否接人,加入了对等待队列大小的判断 state == Statics.ElevatorState.NORMAL && size < 10 。这个策略是在 U2 第一次研讨课上想出来的,也分享给了组员和其他同学,很好的避免了 hw6 中对五台电梯同时维修导致请求堆积而超时的 hack。
除此之外,我的设计中,电梯对于每一次处理的等待请求的选取,也设置了一定的策略。
// ProcessingQueue
private final HashMap<Integer, ArrayList<PersonRequest>> floorIndex;
public synchronized Request poll(int currentFloor) {
...
if (!queue.isEmpty()) {
PersonRequest request;
if (!floorIndex.get(currentFloor).isEmpty()) {
request = floorIndex.get(currentFloor).get(0);
floorIndex.get(currentFloor).remove(request);
queue.remove(request);
} else {
request = queue.get(0);
floorIndex.get(Statics.floorToInt(request.getFromFloor())).remove(request);
queue.remove(request);
}
return request;
}
...
}
通过按楼层索引,优先获取当前停靠楼层的乘客,如果没有,去接第一个调度的乘客。
其实这里的 else 可以设计为接最近楼层的乘客,但是如果最近楼层的反倒会导致行动总长度变长呢,比如电梯停靠在 F2,共两个请求,一个请求是F1->B4,一个是F7->B4。显然先接F7的乘客,下来捎带F1的乘客是更优的,但是如果按楼层最近反倒性能更低。
因此这里选择按最先摁电梯的请求进行处理(符合实际)(其实是懒得写)
但是,实际上这些都可以通过在调度器中使用更好的调度策略来避免,比如通过影子电梯,计算出新请求对于每个电梯新增的最小代价,进行分配,让电梯按照自身等待队列顺序处理,可能效果会更优。
这里只是对我的循环分配进行的一个折中的优化。用较为简单的方式实现一个较为经济的性能。
hw5 中,因为在等待队列中设计了楼层索引 floorIndex,但是取出的时候忘了删,出现了一些幽灵乘客被重复接送的情况,好在提交之前发现并修复了。
hw6 中为了能在捎带的同时,为必定上电梯的主请求留空间,符号写错了。幸运的是,中测和强测都没测到。但是在互测中被不知名同学的不知名样例奇怪地 hack 到了(
hw7 死锁重灾区,在设计电梯不能在二楼冲撞,双轿厢的状态获取,能否强制移动,分配器获取有效状态,分配器能否结束线程,线程自己能否知道自己结束……除此之外,还有分配器无法分配出指令时要 sleep 避免轮询。
hw5 和 hw6 的 bug 相对较容易发现,因为一个简单样例即能测出明显的错误,很容易就 de 掉了。hw7 的 bug 由于多线程的存在,bug不能稳定复现,导致我调试一个 bug 时需要运行好多次,才有小概率抽出那个 bug(
主要原因还是我的锁粒度太粗,导致很容易就死锁了,一开始不太了解多线程,给很多方法都添加了无用的 synchronized 关键字。
本地测试样例主要有 hw6 时五台电梯同时维修的测试,以及 hw7 故意设置分配请求以不同方式在二楼冲撞。

主要在 Unit2 训练项目的架构上进行改造处理,使用 InputThread DispatchThread 和 ElevatorThread 三个线程类同步运行;使用总请求队列 RequestQueue 和等待队列 ProcessingQueue 用于储存各种类型请求的容器。
在 hw7 的迭代中,将大量全局使用的方法和状态提取为 Statics 类降低耦合度,并使用 ElevatorShaft 类为双轿厢电梯提供通信。
在 MainClass 中一次性启动所有线程,包括一个输入线程,一个分配线程和十二个电梯线程,结束条件由每个线程自行判断,由上及下依次结束。
在部分容器的添加和移除方法上加锁,并使用 notifyAll() 进唤醒等待该容器变化的线程。
在分配器的分配方法中,若当前无法分配,应当 sleep(100) 避免轮询,使 CPU 空转。
通过电梯井 ElevatorShaft 类,避免两个电梯互相访问时死锁。
主要使用 DeepSeek 快速模式
我负责设计主要架构,AI负责回答我对线程安全和锁的问题的解答,以及启发我使用电梯井类来进行通信。
大模型在线程安全方面还是比较有优势的,能熟练掌握锁的使用,能发现一些发生死锁的场景,在 debug 的时候功劳很大。缺点就算每次生成的时候都可能使用不同的实现方法,容易忘掉上下文。
感觉现在的大模型想完整生成一个电梯这样的多线程项目还是有一定难度的,仍然需要人手动构建一个合理的架构让 AI 补全。
经历漫长的电梯月,狠狠感受到了多线程项目的复杂度与难度相比原先写的单线程问题要高得多。虽然没有写太复杂的调度策略,但是在线程安全方面是体验满满。由于 bug 不能稳定复现,经常是一个死锁问题能 de 一整天,到最后迭代完,才终于感觉对于多线程与锁的使用初窥门径了。
希望课程组能够开放一部分调度策略的模板,比如在训练项目里加一个调度策略的填空,或者在上机的代码中有所体现。不然在处理多线程本身就已经向当困难,再从零开始写策略感觉略难了。(其实也有问过 AI 如何写,但是 AI 给出的权重也不太合理,和我的架构也不太融洽,最后没有采用)