人脑只有一颗”单线程”的 CPU,却总被要求同时处理很多件事。与其假装自己能多线程运行,不如像单线程 MCU(微控制器)一样,把”同时运行”这件事分解成:任何时刻只有一个任务占用执行核心,其他任务只保存状态,等待被合适的事件重新拉回调度视野。
任务的四状态模型
MCU 里的任务调度器通常会把任务抽象成几个状态。借鉴过来,我把手头的事务分成四种状态:
┌──────────┐ 完成 ┌──────┐
│ RUNNING │ ──────→ │ DONE │
│ 正在处理 │ │ 归档 │
└──────────┘ └──────┘
↑ ↓
调度执行 无法继续
↑ ↓
┌──────────┐ ┌──────────┐ ┌──────────┐
│ READY │←→│ WAITING │ ←→ │ SLEEPING │
│随时可处理│ │等外部反馈│ │ 暂不处理 │
└──────────┘ └──────────┘ └──────────┘
各状态之间的迁移时机会在下文的”中断”部分逐一展开,这里先给出概览:RUNNING 只有两个去向——完成(归档为 DONE)或无法继续(落入 WAITING);READY 中的任务被调度后成为 RUNNING;WAITING 中等待的条件满足后回到 READY;SLEEPING 则在定期检查时被唤醒,移入 READY 或 WAITING。
- RUNNING:此刻真正正在做的事。只允许 1 个。这是单线程的核心约束——既然只有一个执行核心,就不要假装能同时做两件事。
- READY:随时可以被执行的任务。万事俱备,只欠调度。
- WAITING:已经启动,但正在等待外部输入的任务——等别人回复、等实验结果、等快递、等审稿意见。这些任务不需要占用注意力,但需要被记录,这样等到反馈到达时能被准确唤醒。
- SLEEPING:今天不需要处理的任务。除了”三个月后要续费的域名”、”明年想参加的会议”这类远期事项,还包括那些概念上虽然在等外部输入、但好几天内都不可能有反馈的任务——既然 SLEEPING 每天都会被检查一遍,让它们躺在这里就够了,不必占用 WAITING 列表的注意力。
关键的区别在于:WAITING 和 SLEEPING 的任务不占用任何心智资源,它们只是躺在列表里保存状态。你不需要”记住还要等某个人的回复”,只需要信任列表。
中断:什么时候切换任务
MCU 不会漫无目的地轮询所有任务,它靠中断来触发调度。人脑也可以设计几类”中断”,来决定什么时候停下来看看列表、切换任务。
中断一:任务完成或无法继续
这是最自然的切换点:当前 RUNNING 的任务完成了,或者推进不下去了(比如需要等别人提供信息)。严格来说这在 MCU 里不叫中断,更像是任务主动让出 CPU,这里只是借用”中断”的说法作为统一的框架。
切换前,先把当前任务推进到一个可以保存状态的断点,然后按以下流程处理:
- 安置当前任务:如果已完成,直接从列表中移除、归档;如果无法继续,移入 WAITING 列表,并记一笔”在等什么”。
- 检查 WAITING 列表:有没有等待的条件已经满足的,可以移回 READY?
- 从 READY 中选一个任务,切换为 RUNNING。
中断二:Timer Interrupt
进入 RUNNING 状态一段时间(比如一小时)后触发一次检查:
- WAITING 列表里有没有可以移入 READY 的?
- READY 列表里有没有优先级明显更高的任务?如果有,考虑与当前 RUNNING 交换。
当然,人不必像 MCU 那样真的定一个闹钟。更”人类”的做法是把定时检查挂在身体的自然信号上:口渴了去接水、去一趟卫生间,顺便完成一次调度检查。起身活动本来就是该做的事,顺手巡检一下列表,成本几乎为零。
中断三:每日检查
每天选一个固定时间(比如早上刚开始工作或晚上收尾时),检查 SLEEPING 列表:有没有任务该被唤醒了?唤醒后根据情况移入 WAITING 或 READY。
WAITING 和 SLEEPING 的区别就在检查频率上体现:WAITING 里的任务随时可能有反馈,值得在每次任务切换和定时巡检时看一眼;SLEEPING 里的任务今天不需要关心,每天一次的检查足矣。每日检查就是 SLEEPING 列表的唤醒源。
中断四:新增任务
脑子里随时会冒出新的事要做。如果每次都立刻去处理,RUNNING 就被打断了;如果靠脑子硬记,又违背了”不占用心智资源”的原则。
这一类中断与前三类不同:它不触发任务切换,而是处理输入。
解法是引入一个轻量级的 INBOX,作为输入缓冲区:想到什么事,一句话记进去就完事,不做任何分析和分类。
然后在 READY 列表里放一个常驻任务:”处理 INBOX”。等调度到这个任务时,再把 INBOX 里的条目逐条细化——想清楚这件事具体要做什么、下一步行动是什么——然后放进 READY、WAITING 或 SLEEPING 对应的列表。
这样,”捕获”和”规划”被分离成了两个不同时刻的动作:捕获要快、要轻,规划可以等到专门的调度时刻再做。
小结
这套方法的核心可以归纳成几句话:
- 承认自己是单线程的,任何时刻只做一件事,但把这件事做完或做到一个可以保存状态的断点。
- 其他所有事务只以状态的形式躺在列表里,不占用后台心智。
- 靠几类固定的”中断”来触发调度:任务结束、定时巡检、每日检查、新任务入 INBOX。
- 任务什么时候重新进入视野,由事件和时间决定,而不是靠”别忘了”的焦虑。
把四类中断和 INBOX 也画进来,完整的图景如下:
┌──────────┐ 完成 ┌──────┐
│ RUNNING │ ──────→ │ DONE │
│ 正在处理 │ │ 归档 │
└──────────┘ └──────┘
↑ ↓
中断①②:调度 无法继续 中断③:每日检查唤醒
↑ ↓ ↓
┌──────────┐ ┌──────────┐ ┌──────────┐
│ READY │ ←①②→│ WAITING │ ←→ │ SLEEPING │
│随时可处理│ │等外部反馈│ │ 暂不处理 │
└──────────┘ └──────────┘ └──────────┘
↑ ↑ ↑
由 READY 中的常驻任务"处理 INBOX"细化、归类
↑
┌──────────┐
│ INBOX │ ← 中断④:随时想到的新任务,一句话记入,不打断当前任务
│ 输入缓冲 │
└──────────┘
MCU 用几 KB 的内存就能可靠地管理一堆并发任务。人脑的内存虽然更有限,但只要把状态都写在外部的列表里,管理几十件事也绰绰有余。