线程池只有一个工作者,第一条任务已进入27 的受控等待,第二条排队。第三条提交立即得到 BufferError: thread queue is full。取消排队的第二条时 Future.cancel() 返回 True,任务状态成为 cancelled;取消正在运行的第一条时 Future.cancel() 返回 False,但业务层接受了取消请求,状态暂为 cancel_requested。门闩还没释放时,第一条仍在运行;释放后它才完成当前工作单元、停止提交成功结果并变成 cancelled。

这条时间线刻意分开三种事实:Future 是否还能从队列移除、应用是否已接收取消请求、工作者是否已经真正停止。拿一个 True 返回值覆盖三者,会让停机检查误判未结束的工作。

有界的不只是线程数

tasklab.thread_runner.ThreadTaskRunner 用 ThreadPoolExecutor(max_workers=1) 限制工作者数,用 BoundedSemaphore(workers + backlog) 同时约束运行与等待中的 Future 数。这里演示 workers=1, backlog=1,两项容量已占满时第三条不会被悄悄塞进 ThreadPoolExecutor 自身可能增长的内部队列。成功或取消的 Future 在结束回调中释放额度;提交被拒绝不会改变第三条任务的 queued 状态,调用方仍负责以后重试或取消它。

把任务对象的状态读取、修改与快照放在 RLock 同一保护范围内,令 app.run 的“确认 queued → 标记 running”和最终“检查 cancel_requested → 决定取消/成功”不会被同进程另一线程夹断。app.cancel 的状态迁移也使用这把任务锁;query 拿到的是锁保护下的快照。上一章的重复领取反例只是实验,不是生产应用的领取入口。RLock 不跨进程,也不能保护外部副作用,数据库事务仍要负责持久状态。

runner.cancel(future) 分开返回 future_cancelled 与 request_accepted。排队 Future 被取消后,其完成回调同步让业务任务也记成取消;运行中 Future 不能被强行终止,但 app.cancel 会记录 cancel_requested。演示的工作负载在 Event 上有可控制的检查点,释放后 app.run 才看见取消请求并返回 None,结果仍为 null。如果工作单元永远不返回、没有合作式检查点,线程池和 Future 都没有安全的“强杀这条 Python 线程”操作;不能由一次可释放的门闩推断任意工作都能及时停止。

shutdown 的完成条件

ThreadTaskRunner 退出上下文时调用 Executor.shutdown(wait=True, cancel_futures=True):尚未开始的 Future 会被取消,完成回调把对应业务任务记为 cancelled;已经运行的工作者要等到工作单元退出。专门的回归测试让运行工作者等待门闩,在另一线程发起 shutdown,先确认排队 Future 已取消、运行中的 Future 尚未结束,然后释放门闩并检查 shutdown 返回。演示最终从 threading.enumerate() 检查本次带 tasklab-worker 前缀的工作线程已消失。这证明这次进程内停机没有遗留该池工作者;跨进程重启后的遗留 running 与事务恢复要到 35 处理。

1
2
3
cd examples/python-language
PYTHONPATH=src /usr/bin/python3.12 experiments/29_thread_pool.py
PYTHONPATH=src /usr/bin/python3.12 -m unittest discover -s tests -v

本章实验第三条先被拒绝,排队 Future 与运行 Future 分别报告 {true,true} 和 {false,true},释放门闩后状态依次为 cancelled, cancelled, succeeded,最后一个任务使用相同纯 Python 负载而词频结果不变。累计测试核对逐项状态与停机;真实命令、事件顺序、退出码和页面见 examples/python-language/evidence/29/20261007-threads/。池使用进程内 store,不把这份成功证据冒充 SQLite 的跨进程取消或线程性能测量;线程、进程与 asyncio 的相同负载成本要待后续章节与 36 多次测量。

预测题

任务已经进入等待 Event 的钩子,调用 future.cancel() 得到 False,紧接着 runner.cancel(future) 报 request_accepted=True。在 Event 尚未释放时,状态能否已经是 cancelled?

答案:本次路径里先看到 cancel_requested,运行工作单元并未退出。释放之后原 app.run 才到达提交前检查点,返回 None、把状态记为 cancelled。Future.cancel() 对运行中 Future 的失败不代表业务请求一定被拒绝,也不代表可以强制抢占线程。要检查两个时刻的状态,不能只看布尔返回值。

变更题

保留 max_workers=1,删除容量信号量,一次提交十万条被工作者堵住的任务,为什么不能称为“有界队列”?

答案:工作线程数有上限,不等于未处理的 Future 数有上限;若不限制接纳量,排队 Future 及其参数仍可能不断积累。本章容量满时对第三条明确拒绝并保留原任务状态,能用两个被占用的槽位和第三条 BufferError 验证;十万条的内存峰值没有在本章实际测量,不把风险当成已有基准数据。

30 将以相同文本和 CPU 工作负载增加独立进程策略,区分序列化参数与共享内存。系列导航只包含通过页面检查的内容。

参考资料