La perfection est atteinte, non pas lorsqu’il n’y a plus rien à ajouter, mais lorsqu’il n’y a plus rien à retirer. (完美, 不是指再无任何东西可以添加, 而是指再无任何东西可以减去.)

— Antoine de Saint-Exupéry, Terre des hommes (《风沙星辰》)

任务背景

本单元以某公司大楼的目的选层电梯系统为背景, 根据需求与正确性约束, 设计多线程程序来模拟该电梯系统的运行.

程序结构分析

文件结构

oo
├── App
├── queue
│   ├── CarryingQueue
│   ├── Passenger
│   └── WaitingQueue
├── runnable
│   ├── Dispatcher
│   ├── elevator
│   │   ├── Elevator
│   │   ├── Shaft
│   │   └── State
│   └── Inputer
├── strategy
│   ├── dispatcher
│   │   ├── DispatchStrategy
│   │   ├── MagicNumberDispatchStrategy
│   │   └── RoundRobin // 轮询, 仅在个人测试时使用过, 并未在实际测试中使用.
│   └── elevator
│       ├── Advice
│       ├── ElevatorMoveStrategy
│       └── LookStrategy
└── utils
    ├── FloorUtil
    └── OutputUtil

我们先从项目的文件结构开始入手, 分析一下程序的主要包和下属的类, 分析它们作为不同的逻辑群, 在整个项目的身份与地位, 以此来一窥整个程序的结构.

主要的包为:

  • queue: 关于容器逻辑的包, 其内为对 PersonRequest 的包装, 以及承载 Request 的容器类 CarryingQueueWaitingQueue.
  • runnable: 关于线程对象逻辑的包, 包括 Inputer, DispatcherElevator 三个 Runnable 对象和其相关逻辑.
  • strategy: 关于具体运行策略的逻辑的包, 包括 dispatcher 包, 负责调度逻辑; 以及 elevator 包, 负责运行逻辑.
  • utils: 盛放工具类的包, 包括对输出逻辑进行封装的 OutputUtil 和对楼层运算逻辑封装的 FloorUtil.

逻辑组织

The big idea is ‘messaging’ - that is what the kernel of Computing is all about…
The key in making great and growable systems is much more to design how its modules communicate rather than what should be their internal properties and behaviors.

— Alan Kay

下面我们来具体分析不同的逻辑群之间, 以及逻辑群内部是如何进行通信协作的.

大体上, 该程序可以分为三层: 数据生产层, 中枢调度层, 物理执行层.

一般请求

我们首先针对一般的请求, PersonRequest 进行讨论.

我们的程序是标准的两级生产者-消费者架构:
一级生产者, 以 inputer 为核心的数据生产层
一级消费者兼二级生产者, 以 dispatcher 为核心的中枢调度层
二级消费者, 以 elevator 为核心的物理执行层.

进入到每个层级:

对于后两层, dispatcherelevator 都采用策略类的方式.
将具体调度策略或者电梯运行策略分离出去, 而类内部只实现所有必要行为, 随后运行时将状态交给策略类, 获得指令并执行.
如此设计, 既让这两个类内部逻辑更加简单, 只需机械执行, 也很好地符合了开闭原则, 可以让运行策略独立于运行逻辑实现, 方便地更换或者调试策略.

在第三层, 还有为了防止轿厢碰撞而专门设计的管程 Shaft.
在作业中, 负责控制同一时刻, 一个井道最多有一个轿厢处于 F2, 并且记录当前有无电梯请求进入 F2 或正处于 F2, 帮助电梯相互申请, 协调 F2 资源的占有和释放.
而如果对于可能的迭代需求, Shaft 事实上也能作为 elevator 间通信的中介, 起到更多通信作用.

而在层间, 则是同一种容器的复用: WaitingQueue, 这一类是主要的共享数据类, 涉及到一些同步问题, 还会在后续篇幅进一步讨论.
但是简单来说, 这一类就是盛放待执行的乘客请求的容器类, 实现了请求的存储, 粗取(提供给 dispatcher 取出一个请求分配)细取(提供给 elevator 取出符合要求的请求执行).

inputerdispatcher 间通信中使用的 globalQueue 是一个 WaitingQueue 的实例, inputer 解析出来的请求都会存入 globalQueue,
dispatcher 只需要在 globalQueue 非空的时候取出一个请求, 交给 MagicNumberDispatchStrategy 对不同电梯的状态进行打分, 然后交给最好的电梯即可.

dispatcher 把请求交给电梯的过程, 也即这两者之间的通信, 则涉及每个电梯自己的 WaitingQueue 实例.
dispatcher 会把对应的请求直接加入对应电梯的 WaitingQueue 实例, 等待电梯进行处理.

elevator 会将自身状态交给 LookStrategy, 获取下一步行为, 例如移动或者接送乘客.
其中, 对于接客, 就是从与 dispatcher 共享的 WaitingQueue 实例中取出符合条件的请求, 随后加入内部私有的非共享资源 CarryQueue 中管理.
如果发生特殊请求需要将 RECEIVE 取消, 将轿厢内乘客遣散, 则是从电梯 WaitingQueueCarryQueue 中取出所有请求, 并且把未完成的重新加入 globalQueue, 等待 dispatcher 重新分配.

因此, 通过 WaitingQueue 类, 我们串联起来了三层逻辑, 让三者之间的通信变得有序, 每个层级的职责范围确定.
每个层级只需要对每个乘客做好本层级的工作(解析, 调度, 执行), 无需关注自己的请求从何而来, 更无需关注自身的工作完成后, 流往别层的请求会怎么样.
由此, 让乘客请求得以在此间正确流转与回流, 妥善地避免了请求丢失等情况发生.

但事实上, 这里也有设计上的一个缺陷, 这两处的 WaitingQueue 虽然有一些共同之处, 例如请求的存入逻辑, 队列关闭标记等是一致的, 但是两层之间对队列具体的要求还是不同的.
对于 dispatcher, 队列只需要有粗取功能就好, 并无必要按楼层分别存储请求, 反而徒增遍历取请求的复杂度, 为 elevator 准备的细取和请求回流逻辑也是完全无用的, 因此是大材小用了.
而对于 elevator 而言, 这个队列还算是合格, 但是专门为 dispatcher 实现的粗取功能, 是完全多余的.

这里我最开始自认为秉持着 DRY 的原则, 由于两类在迭代初期, 大量逻辑的偶然的相似性, 加之”在哪里排队都是排队, 无论是大厅还是电梯前”的朴素思想, 因此将之合并.
而随着后续回流逻辑的增加, 两者使用逻辑的不同, 实际地位的不对等逐渐显现, 也因此让这个类实际承担了两种不同的职责, 每一个类都只会用到其中部分功能.

因此, 我想, 这告诉我们架构的设计应该从实际功能与业务本质出发, 而不是完全拘泥于有无重复逻辑. 毕竟世界上天然的有很多不同的事物, 其实在很多方面是类似的, 但正是其不同的地方得以使我们区分开来.

更进一步讲, 每个类进行专精式的设计, 才能更好地服务于其特殊需求:

  • 单独为 dispatcher 创立的 GlobalQueue 类可以线性排列所有请求, 快速让其取出一个进行调度; 可以快速插入回流请求, 而不关注来源.
  • 单独为 elevator 创立的 LocalQueue 类可以按出发地分层承载请求, 甚至缓存当前存在请求的最高和最低楼层, 进一步优化性能等.

或许在目前压力较小, 简单的程序, 目前融合的设计也能胜任工作, 但无疑, 隔离接口与功能特异, 才能有更优雅的架构.
或者说, 我们面临无数的开发原则, 有时候看似是相互冲突的, 这时候不妨从业务本质入手, 找到真正在此处适用的原则.

特殊请求

而针对于特殊请求 MaintRequest, UpdateRequestRecycleRequest, 代码中有另一条”加急”的逻辑通路.

因为这一类特殊请求有响应时间的需求, 因此不能与一般的 PersonRequest 放在同样的队列慢慢等待处理,
而是一种异步的请求, 随时可能到来, 而且要在接到的一瞬间立即着手准备.

如果类比的话, 这就像是 CPU 的中断, 也因此我采取了类似地思路解决.

inputer 在解析到此类指令后, 会像是按照 Execode 查找异常处理向量一样, 会直接调用 dispatcher 的对应方法;
dispatcher 会直接调用电梯的对应方法, 让电梯进入特殊状态, 开始进行异常状态的处理, 也就是维护, 升级, 回收任务之一.

而与 CPU 异常处理程序一致的是, 电梯的特殊状态, 也都是采取大致固定的流程: 去往固定的楼层, 执行相对固定的流程, 而在此过程中不响应(对于电梯, 也不会有)新的中断.
因此我在这里对于每一个特殊流程都硬编码了不可中断的处理流程, 符合逻辑直觉, 也方便管理, 使得 dispatcher 自然无法为这些电梯下达新的指令.

这就展现了我 elevator 状态机的设计思路: 少状态, 无记忆.

public enum State {
    NORMAL,
    DEACTIVATED,
    MAINTAINING,
    UPDATING,
    RECYCLING;
}

电梯仅仅有五种状态, 其中后三种都是特殊处理流程, 期间自动顺序执行所有工序, 不响应请求, 完成后自动回到应当回到的 NORMAL 或者 DEACTIVATED;
NORMAL 状态下, 则根据 LookStrategy 的指令, 无记忆地取指执行任一动作 move, open(包括上下客与关门), reverse, yield(离开 F2). 至于 DEACTIVATED 状态, 则是副轿厢的初始状态. 在程序一开始就创建好相关实例, 如果处于该状态就跳过分配打分, 从而无需维护更复杂的电梯升级逻辑.

这是一种我认为不错的设计取舍, 相较于细分状态, 例如开门状态, 关门状态, 移动中状态等, 有一定的优势.

这样的设计无疑大幅简化了 elevator 的复杂状态转移, 以及 LookStrategy 的下一步动作确定.
在粗分状态, 聚合化动作的情况下, 这些动作都是不会相互影响的, 因此不需要考虑上一步动作是哪类, 而只需要聚焦于当前的状态决定要执行的动作.
自然也不需要按照状态, 根据状态转移图来执行, 实际上是让 elevatorLookStrategy 要关注的信息更少了.
以此, 使得代码更符合人的认知, 可读性好, 甚至不需要分析状态转移方程, 极大降低了开发压力, 也减少了代码复杂度.

而相反, 我认为原子化的状态更适合一个在多状态下有不同响应逻辑的, 并且是由记录状态的需求, 根据历史决策来决策的程序.
如果原子化状态, 可能面对未来类似地需求会更加得心应手.
但是…

Rule of Simplicity: Design for simplicity; add complexity only where you must.

— Eric S. Raymond, The Cathedral and the Bazaar

对于我们的任务而言, 粗粒度的状态已然足够, 甚至开发起来认知负担小, 更快且更不容易出现错误.

其余内容

以上是项目的主要逻辑架构, 其余的就是在主线之外的工具类 FloorUtilOutputUtil.
前者负责把请求中的字符串转化为整数, 并且掌管楼层计算比较等功能, 实际上将楼层封装, 使得其余组件不必手动计算楼层关系等.
后者即为对 TimableOutput 按照要求的输出格式进行封装, 使得其余部分代码更简洁.

同步块与锁

A Java programmer, a C++ programmer, and a multithreaded programmer walk into a bar.
The multithreaded programmer says, ‘Wait, I’m already here!’

既然是多线程的程序, 则必然涉及同步块与锁的设计. 上锁与否, 锁的粒度大小, 给哪些实体上锁, 何时上锁解锁等等, 都是不能不考虑的课题.
问题的关键在考虑共享资源的读写冲突, 以及竞态条件.

在我的程序中, 我尽量控制了锁的使用, 尽力做到了面向数据的加锁, 竞态条件的加锁, 物理资源的加锁, 以及避免不必要的锁.
因此锁的关系极其简单, 而且也只使用 synchronized 来修饰方法, 或者 synchronized 代码块,
完全不涉及任何嵌套锁或者资源释放问题, 这使得我基本没有发生过线程冲突问题.

面向数据的加锁: WaitingQueue

首先是作为共享资源的 WaitingQueue, 无论是在 inputerdispatcher 之间作为全局队列, 抑或是在 dispatcherelevator 之间作为局部队列,
都是由两个实例所共享的, 需要同时读取, 修改. 因此这一类所有的对外公开的方法都是 synchronized 修饰的, 包括:

  • 读取当前请求队列状态: 整体有无请求的 isEmpty, 某层有无请求的 hasRequest, 有无可接入电梯的 hasPickUp, 请求队列是否结束的 isEnd, 获取请求数量的 getTotalRequestCount.
  • 插入取出队列中的请求: 加入请求的 addRequestinsertRequest(s)(前插), 取出请求的 pickUpBefore, 在 RECEIVE 结束时清空队列的 cancelDispatch.

这些框架都主要在第一次迭代便确定, 此后再无大规模修改. 通过使用 synchronized 修饰方法作为此类的唯一上锁方式, 也避免了多种锁机制的认知负担.
作为在程序多处使用的多线程间的共享对象, 通过对读写进行严格的锁限制, 进而确保了不会在线程间冲突.
这也使得上层的对象, inputer, dispatcher, elevator 在读写 WaitingQueue 实例的时候无需担心线程冲突问题.

当然, 为了避免竞态条件, 一些 synchronized 块也是必须的,
例如 dispatcher 在每次判断队列有无请求, 是否结束, 再到取出请求的多个过程, 需要对队列整体上锁, 防止判断后, 在修改时队列条件与此前不一样的 Check-then-Act.
都是较为容易发现的竞态条件, 可以轻松处理.

同时, WaitingQueue 作为数据流转的核心, 全项目也主要依靠对该队列锁的获取与放弃, 来进行线程通信和休眠.

例如 dispatcher 在没有队列没有请求时, 会放弃持有 globalQueue(WaitingQueue 的全局队列实例) 的锁,
等待 inputer 放入请求, 或者来自 elevator 的请求回流, 通知其重新开始运作, 进行分配任务.

又例如 elevator 在没有请求需要处理的时候, 也是通过调用 waitingQueue(WaitingQueue 的局部队列实例) 的锁,
等待 dispatcher 分配请求, 或者来自 dispatcher 通过捷径传递的加急特殊请求, 来通知其重新开始取值执行的循环.

再例如 dispatcherelevator 传递加急请求后, elevator 将自身目前任务返回 globalQueue,
此时通过 globalQueuedispatcher 进行唤醒, 防止其正处于休眠状态, 没有及时分配返还的请求.

如此藉由数据结构加锁与放弃持有的通信机制, 不需要对 dispatcher 进行上锁, 大部分时间也不需要对 elevator 上锁, 彻底避免了可能发生的嵌套锁导致的死锁.

竞态条件的加锁: elevator

尽管 elevatorWAIT 状态也是通过对 waitingQueue 调用 wait() 方法来休眠, 并且依靠对 waitingQueuenotifyAll() 唤醒,
但是 elevator 自有其特殊之处: 状态改变和任务分发的互斥, 即题目要求电梯在进入特殊状态时不能再接受请求.
因此如果对电梯分发请求的 Check-then-Act 中, 对 elevator 不加锁, 则可能在判断时判断为”NORMAL 状态, 可以分配”, 而分配前 elevator 突然改变状态, 造成错误.

因此事实上只需要对这种危险的竞态条件进行细粒度上锁控制: dispatcher 对电梯进行判断与分配时上锁; elevator 改变自身状态为 MAINTAINING 等特殊状态时上锁,
即可消除上述的竞态条件, 并且锁的粒度足够小, 只需要覆盖几行逻辑, 在内部也不涉及其余锁的获取, 并不会造成死锁等.

除此之外, elevator 并无任何 synchronized 方法, 或是其余类型的锁, 因为其修改自身其余状态, 并不会涉及线程冲突, 唯一要注意的就是数据可见性, 将在后面讨论.

物理资源的加锁: Shaft

普遍而言, 一个大楼最多有一个 F2, 因此这决定了 Shaft 作为物理资源的特殊性: 唯一性和独占性.

诚如前所述, Shaft 实质为管程, 作为每个井道唯一的物理资源, 任意时刻最多一个 elevator 占据, 从而负责同一井道的两个 elevator 的通信, 尤其是在 F2 的协商.
因此其所有的公开方法都是包私有的, 而无 public, 从而进一步抽象, 使得外界调度等甚至无需知晓其存在, 从而满足了最小可见性原则.

而由于其负责的任务涉及线程间冲突, 势必需要对方法进行 synchronized 的修饰(同时为了避免认知复杂, 这也是此类唯一的上锁方式).

其思路为: 通过两对布尔值, 记录上下两部电梯是否有任何一部正在占据 F2, 以及是否有任意一部试图进入 F2. 随后, 每当电梯经过 F2 时, 进入时调用 moveToF2, 自动管理上述两对布尔值, 作为仲裁者, 在需要时唤醒另一部电梯, 使其避让; 离开时调用 leaveF2, 清除占有信息.

最后, 其还为电梯提供了 shouldYieldF2 的方法调用, 在每次 LookStrategy 进行行动决策时进行参考, 从而及时避让.

因此两个轿厢对 F2 的请求通过意向, 主动避让, 避免他们互相改变状态断绝其互锁可能性.

避免不必要的锁

The best lock is the one that doesn’t exist.

— Doug Lea

私有资源: CarryQueue

CarryQueue 是每个电梯所私有管理的乘客队列, 只有线程内部才可以进行修改, 外界唯有调度时考虑负载时可以读取缓存的 currentWeightcurrentPassengerCount.
因此不涉及多线程读写冲突, 自然无需上锁.

避免锁膨胀: volatile

事实上, 不是所有的内容都需要强同步, 调度系统正是如此.
调度系统只需要阅读关于 elevator 的一些状态, 而不会修改, 故不会造成读写冲突; 同时, 在调度系统读取状态时, 一些状态, 例如负载, 发生一些改变也并不致命.
因此对于这些需要获取的状态, 我们大不必为 setter 和 getter 都奢侈地给上一个 synchronized, 而是给那些状态一个 volatile, 确保状态实时更新即可.
不然, 即使我们确保调度系统每时每刻都能读到最准确的状态信息, 或许做出的调度优化, 也并不比减少锁的性能损耗高出多少, 却还要增加更多开发负担.

无锁化全局状态: AtomicInteger

最后, 这里的设计是一个防御性的, 在我某一瞬间突然意识到: 回流的时候, 我们会先把请求从 elevator 的队列中取出, 此时全局队列与局部队列中均不存在这些请求.
如果面对一些输入顺序, 以及在 CPU 未知的调度顺序下, 是有错误判断所有请求处理完毕, 导致线程提前关闭的风险的.
尽管风险很小, 但是只需要加一个变量维护当前未完成的请求数, 在加入请求和去除请求时多加一行维护的代码就可以完全避免, 何乐而不为呢?
毕竟…

Anything that can go wrong will go wrong.

— Edward A. Murphy Jr.

调度器设计

正如前面所述, 我的 dispatcher 通过 WaitingQueue 的实例, 以及一条捷径, 与 inputerelevator 交互, 表现为:

  • 对上游放入 globalQueue 的普通请求, 向下游分配到对应电梯的 waitingQueue.
  • 对上游传达的特殊请求, 直接通知下游对应 elevator 线程处理.

而其余线程通过 globalQueuedispatcher 通信, 唤醒调用了 wait()dispatcher.

而调度策略, 正如其名所言 MagicNumberStrategy, 是通过一系列经验魔数, 对电梯进行打分分配的策略.

  • 首先是 Fail Fast, 对于以及处于维护等特殊状态, 处理的请求过多的, 无法接到请求的 elevator, 直接给予最差分数.
  • 其次是根据距离, 运行方向, 是否需要调头, 当前 carryQueue 的负重和人数, waitingQueue 的人数, 综合给出分数.
  • 取分数最小的一个电梯. 特别地, 如果是最差分数, 则认为不分配. 如果 dispatcher 接收到不分配的指令, 那么进行休眠, 等待电梯状态改变后, 会被 elevator 唤醒.

这一套方案有两个优点:

  • 不需要具体计算耗电量等因素, 只需要根据生活经验和直觉, 找出需要考虑的影响因素, 并且给出合理的权重进行打分, 随后通过一些样例测试调整即可.
  • 实现简单, 只需要进行判断和计算, 给出最终得分, 程序会自然找出公式下的最优解并且运行, 而即使打分有微小的偏差, 一般也不影响正确性.

Bug 与 Debug

在本单元的中测, 强测以及互测中, 程序均未出现 bug.

而就开发阶段而言, 由于程序架构和逻辑都比较简单, 也少有并发问题, 只是一些边界情况出现 bug 或者不理想的情况.
例如在第二次作业调度策略开发中, 我在思考边界条件时, 发现可能出现所有电梯都在维护时, 所有请求去往同一电梯, 有潜在超时风险.
在第三次作业开发中, 我意识到回流的时候, 我们会先把请求从 elevator 的队列中取出, 此时全局队列与局部队列中均不存在这些请求, 可能使得程序过早停止.
这些情况有些易于构造样例, 可以按照思考的解决方法处理后自行测试; 有的情况很难构造样例, 甚至是概率性的 bug, 那么就做好防御性编程准备, 确保尽可能安全.

而有时, 出于一些失误, 可能造成方法逻辑出现问题, 例如第三次作业换乘时, 我把所有换乘乘客当作已完成请求处理了.
此时只需要在发现问题后, 追寻相关逻辑链条, 检查代码, 并且打上 log, 打印一些关键变量, 帮助定位.

更进一步, 在此之前, 我也会先把代码发送给大模型进行一次初筛. 这不能保证发现所有 bug, 也不能保证发现的都是 bug.
因此面对其回答要进行妥善思考执行逻辑, 无视大模型的误判; 修改大模型发现的 bug; 并且不能过于依赖, 在此之后进行如上所述的个人测试.

线程安全与层次化设计

Programming as if the person who ends up maintaining your code will be a violent psychopath who knows where you live.

— John Woods

层次化设计是手段, 而非目的. 进行层次化设计是为了提升项目的拓展性与可维护性.
也就是, 通过层次化设计, 把业务全流程分为不同的逻辑区域, 分别设计, 然后互相通信, 协同完成任务, 做到高内聚低耦合, 可以让我们的项目更容易地被维护.

但是层次化不是完全为了重构或是拓展准备的, 其实更是解决很多 bug 的绝佳手段.
也就是提供一个良好的, 逻辑清晰的, 便于维护的层次化设计程序, 我们往往可以直接避免大多数线程安全问题, 而非在出现后再去想办法解决.

例如在代码中, 分好三层生产消费关系, 从而做好数据隔离:
区分出共享资源(例如 WaitingQueue), 仔细上好锁, 防止读写冲突;
而对于私有资源(例如 CarryQueue), 直接当作单线程的程序来完成即可, 完全避免锁的开销(而且不使用锁就不会有锁的风险)
而层级间便依靠这些共享对象进行通信, 这使得线程间通信变得更加简单, 并没有那么多对象需要那么多锁, 而是集中于共享对象, 专注于一个类的数据共享和读写冲突, 这往往可以降低开发难度, 从而使我们更容易写出安全的代码.

又例如分好执行层和策略层关系, 除去我们在文章开头叙述的符合开闭原则, 更重要的是使得状态机化繁为简, 执行层不需要关注过多信息, 只需要完全按照策略层的指挥执行即可.
以此, 我们将一个复杂的类, 分为了两个相对简单的类:
策略层也因此变得相对简单, 读取状态, 给出指示, 而不需要考虑线程间冲突; 执行层按部就班, 并且做好和其他线程的通信, 却并不必关心自己为什么如此行动.

因此通过这种层次化, 可以最大限度减少我们要同时关心的内容, 并且梳理好各个部分的职责, 以及通信关系, 从而避免面对了很多潜在的线程安全问题.

大模型的使用

在开发中, 我主要使用 Gemini 3.1 Pro Preview(AI Studio).

在与大模型的合作中, 我负责给出具体架构与实现思路, 而大模型负责为提供代码大致模板, 或是补全的服务.
这有两点原因:

  • 此前, 我对多线程编程也是知之甚少, 很多方法的调用, 或者具体代码模板, 书写技巧并不熟练, 此时通过询问大模型可以在做中学, 防止自己在代码实现层面出现过多初学者的 bug.
  • 大模型对于作业中的特定领域理解尚浅, 面临我们作业的一些特殊业务逻辑往往会产生幻觉, 胡乱分析. 例如其常常把我的性能优化策略当作 bug 指出, 因此如果反之, 使其作为主力则效果不会过于理想.

另外, 大模型也会帮助我进行代码初步审查, 在写完后先交给大模型检查, 能发现一些比较浅显的 bug 或者失误, 快速改正, 而不必自己一个个排查.
但同样的, 面临一些极端条件的考量, 大模型也不如自己独立思考, 针对问题分析并尝试, 因此在大模型初筛之后, 对程序的优化, 潜在 bug 的排除才算是刚刚开始.

体验与感受

多线程程序设计对我而言无疑是一个崭新的体验,
从对多线程编程的一些耳闻, 到在做中学, 懂得多线程编程的基本接口和常用架构;
再到抽丝剥茧地分析业务, 对架构形成自己的思考, 并逐步实现;
同时分析与解决更加复杂且难以捕捉的并发 bug.
这一套流程的开发体验都是走出舒适圈, 并且对新领域的探索, 既让人感到新奇有趣, 又可以给代码能力和架构能力带来更大的提升.

最后, 对于课程, 我有两个想法:

  • 在指导书必要的位置提供双链跳转链接. 因为多线程程序的运行约束较多, 状态机比较复杂, 因此指导书大量位置都是对其余位置的引用, 即”见…处”,
    这会略微造成阅读困难, 但是如果”见…处”提供一个跳转至那个位置的链接, 则会对同学们理解任务和约束提供极大帮助.
  • 考虑使官方包更为轻量化, 例如 Request 等类只提出基本约束, 让同学自己实现其具体细节, 从而这样可以使得设计更加架构特异性, 方便同学开发,
    而不必特地写包装类处理, 导致相关逻辑复杂化.(但不知道是否会对评测造成不便影响, 因此只在此提出个人见解, 以供参考)