开始前,会这些就够了
- 认识请求与响应;能运行一个 Python 文件
遇到不熟悉的词,先看这 3 个解释
- 幂等
- 在约定的范围内,把同一个请求做一次或重复做多次,对目标业务产生的预期效果相同;它用来防止重复执行把一件事变成额外多办几次。
- 超时
- 调用方为一次等待设置上限,超过时间还没拿到所需结果,就结束这次等待并报告超时;这样能限制资源占用,但并没有自动确认对方最终做到了哪一步。
- 取消请求
- 取消是向正在执行的工作发出停止意愿,再由相应代码或运行环境响应。调用方停止等待、服务端收到信号、真正停止后续动作,是需要分别观察的步骤。
你点了导出,页面显示超时,过一会儿后台却仍生成了文件。先别急着判断“超时没有生效”:页面停止等结果与执行者停止工作,本来就可能是两个动作。
这与“请求超时后是否重复下单”相关,但本篇把焦点放在执行本身。下面用真正启动的 Python 工作线程观察,而不是靠睡眠长短猜哪件事先发生。
把三个时刻分开记
调用超时后,先分清楚究竟发生了哪件事:
| 时刻 | 发生了什么 |
|---|---|
| 调用者不再等待 | 调用者结束了等待 |
| 发出取消请求 | 调用者请求工作方停止 |
| 工作方实际停止 | 工作方收到信号、检查状态,并停止工作 |
这三件事可能先后发生,也可能某个 API 只完成其中一步。
函数名里写着
timeout或cancel,仍要读清它究竟控制谁。不再等待、请求取消、实际停止,不能混为一谈。
取消信号到了,处理函数仍需配合
例如,gRPC 可以将取消通知传给服务端。但官方指南说明,应用自己的处理函数仍需配合停止。
- 长期计算:如果处理函数从不检查取消状态,不能期待框架替它安全地中断每一条业务指令。
- 继续调用下游:还要核对当前语言的实现是否会自动传播取消,不能只看上游已经发出了信号。
等到线程真正开始,再让等待超时
程序用 started 确认工作已经开始,用 release 把它暂时停在一个可控位置。主线程尝试立刻获取结果,发现还没完成便超时。随后检查工作仍在运行,再对这个已经运行的 Future 调用 cancel。
最后显式放行工作,再取回结果。Python 这个接口不能用 cancel 取消已经运行的调用,所以这里会得到 False。它不代表所有语言和 API 都有相同规则;程序验证的是 concurrent.futures 的这一项约定。
from concurrent.futures import ThreadPoolExecutor, TimeoutError
from threading import Event
started, release = Event(), Event()
def work():
started.set()
if not release.wait(5):
raise RuntimeError("demo release was not received")
return "finished"
with ThreadPoolExecutor(max_workers=1) as pool:
future = pool.submit(work)
try:
if not started.wait(5):
raise RuntimeError("worker did not start")
try:
future.result(timeout=0)
except TimeoutError:
print("caller stopped waiting")
print("worker still running:", future.running())
print("cancel running work:", future.cancel())
release.set()
print("later result:", future.result(timeout=5))
finally:
release.set()
怎样运行:Python 3.10+ 标准库;保存为 timeout_cancel.py,运行 python timeout_cancel.py。
运行后对照结果
caller stopped waiting
worker still running: True
cancel running work: False
later result: finished
能停止,要靠执行路径愿意配合
假设导出按批次处理记录,可以在每批开始前检查取消标记,停止领取下一批,并在清理阶段释放自己借用的资源。检查间隔决定有多快能响应;若正在等待一个不响应取消的外部接口,还得核对那个接口自身的超时与退出办法。
本例的 release 只是让实验可重复的门闩,没有实现这种分批取消协议。若要扩展,可以加入“取消已请求”和“工作已结束”两个独立状态,再分别观察它们,避免把通知发出当作全部工作已经停止。
停止后续工作,不会自动撤销已经完成的事
假设任务已经提交一笔数据库修改,再收到取消信号。停止后续步骤不等于那次提交被撤销。需要恢复业务状态时,应先确认哪些动作已完成,再按业务设计补偿或查询;别对外直接宣称“取消成功,所以没有任何效果”。
程序退出时的等待也要分开理解。本例使用线程池的 with 语句,会在离开时等待工作收尾。主线程某次取结果超时,不表示线程池立即消失。演示中的五秒是避免实验异常时无限卡住的保护等待,不是实测耗时。
记住这三点
- 停止等待、发出取消与执行停止,是可以分别观察的动作。
- 运行中的工作如何响应取消,取决于 API 和业务代码的配合。
- 发出取消请求,不代表已提交的数据修改或已经完成的操作会自动撤销。
别把例子用过头
- Python
Future.cancel的运行中返回 False 不能推导为所有取消 API 的规则。 - 程序只涉及本地线程,没有启动
gRPC服务或检验远程传播。 - 放行门闩用于控制实验,不等于实现了可取消任务框架。
自己试一小步
把工作改成三批,每批前检查一个取消标记,应如何证明它没有开始后面的批次?
- 分别记录取消发出、每批开始、每批完成和工作退出四类事件。
- 在第一批之后发出取消,并用门闩固定这一时刻,避免用随机睡眠猜顺序。
- 再把取消移动到最后一批完成之后,检查同一份记录能够证明什么。
做到这些,就算完成
- 没有把“收到取消”与“撤回此前所有效果”混为一谈。
- 异常路径也会释放门闩并等自己创建的线程退出。
- 记录足以判断哪些批次开始过,哪些仍无法确定。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- gRPC:Cancellation (新标签页)↗
Overview、Cancelling an RPC:取消信号、应用处理函数配合、下游传播随语言不同。
- Python 3.10:concurrent.futures (新标签页)↗
Future.cancel、Future.result与Executor.shutdown:运行中取消限制、等待超时与上下文退出。
找一份资料继续练
先选一项,按下面的范围继续学;不必从头读完整门课程。
Go 官方短读:把取消信号一路传到数据库调用 (新标签页)↗
沿 Context、QueryContext 和 cancel 看取消信号怎样传下去,再核对接收方不支持取消时为什么工作仍可能继续。
查看学习准备与访问说明 →