JavaLYG

Python线程池result超时后还在运行:用取消状态与协作停止分清边界

给线程池任务调用result(timeout=几秒),捕获到TimeoutError后,请求返回了,后台任务却还在执行。这并不矛盾:超时的是这次取结果的等待,不是工作函数的寿命。把两件事混在一起,最容易留下重复写入和退出卡住的问题。

下面只用标准库做本地隔离实验,受检解释器为Python 3.14.3。不连接外部接口,不操作数据库;结论限于Future状态和线程池收尾,不代表任何真实业务已经回滚。

一、先分清:等不及,不等于任务停了

Future.result(timeout)等待任务完成并取回返回值。如果等待期限内仍未完成,它抛出TimeoutError,但不会因此向工作线程注入异常,更不会自动撤销函数已经做过的操作。下一次调用result,仍然可以拿到同一个任务后来产生的结果。

timeout=0很适合验证边界:不为这次读取继续等待。前提是先确定任务确实正在运行,否则任务可能恰好已经结束,实验就会变成拼运气。这里让工作函数发出启动信号,再停在另一个Event上,主线程收到信号后才取结果。

二、cancel要看状态,done不能当成功

cancel尝试取消尚未执行的调用。已经运行或已经结束时,取消失败,返回False;取消成功则返回True。False只说明没取消成,并不能单独证明任务还在运行;判断当前是否执行,要另外看running。实际业务里状态会继续变化,连续查询不是一份原子快照。

done表示成功取消或执行结束,里面既有正常返回,也有异常结束。它像“这一单不再流转”,不是“这一单办成了”。成功取消的Future再调用result会抛CancelledError,工作函数异常结束则由result重抛业务异常。

现象实际边界下一步
result等待超时本次没拿到结果查询任务结果,别当成已终止
cancel返回False运行或结束,未取消结合running和结果判断
cancelled为True调用被成功取消处理CancelledError
done为True取消或执行结束区分返回值与异常
shutdown不等待不阻塞当前收尾调用仍需处理运行中的任务
Python线程池等待超时与成功取消的两种替代案例
仅讨论ThreadPoolExecutor的Future;上排另调cancel(),不是超时自动取消。False也可能是任务已经结束,不能反推仍在运行;下排以成功取消为条件。原图由MiniMax H3标准版1K生成,等比处理为1600×900后精确补充API范围与条件;经技术与可读性复核。

三、完整实验:用Event锁住关键时刻

把下面完整代码保存为sample.py,使用Python 3.9及以上运行。等待异常从concurrent.futures单独导入;它从3.11才成为内置TimeoutError的别名,不能把新版捕获方式直接套给旧版。这里只实跑3.14.3,未测试旧解释器。单worker保证后提交的任务排队;前一个任务不放行,后一个就没有机会先跑掉。检查使用if和异常,不依赖assert,因此开启优化也不会把验收条件删掉。

工作函数最多等十秒,这是实验失败时避免挂住的兜底,不是被测的result超时。会合失败会明确报RuntimeError。每个阻塞点都在finally中释放,再等待线程池回收;控制线程也会join,避免把“主线程走完了”误当成资源清理完成。

from concurrent.futures import ThreadPoolExecutor, CancelledError
from concurrent.futures import TimeoutError as 等待超时
from threading import Event, Thread
import sys

次数 = 0

def 检查(条件, 说明):
    global 次数
    if not 条件:
        raise RuntimeError(说明)
    次数 += 1
    print(f"通过{次数:02d}:{说明}")

def 会合(事件):
    if not 事件.wait(10):
        raise RuntimeError("会合失败,非实验中的等待超时")

def 工作(已启动, 放行):
    已启动.set()
    会合(放行)  # 有限等待兜底,不用睡眠猜测线程状态
    return "收到信号,正常返回"

def 应抛(类型, 调用):
    try:
        调用()
    except 类型:
        return True
    return False

# 一:运行中超时、排队取消与协作停止
启动, 停止, 排队已跑 = Event(), Event(), Event()
池 = ThreadPoolExecutor(max_workers=1)
try:
    运行 = 池.submit(工作, 启动, 停止)
    会合(启动)
    排队 = 池.submit(排队已跑.set)
    检查(应抛(等待超时, lambda: 运行.result(timeout=0)), "result等待超时")
    检查(运行.running() and not 运行.done(), "超时后仍运行且未结束")
    检查(运行.cancel() is False, "运行任务cancel返回False")
    检查(排队.cancel() is True, "排队任务cancel返回True")
    检查(排队.cancelled() and 排队.done(), "取消也是done")
    检查(应抛(CancelledError, 排队.result), "取消后result抛CancelledError")
    停止.set()  # 任务主动响应;不是强杀线程
    检查(运行.result(10) == "收到信号,正常返回", "协作停止正常返回")
    检查(not 运行.cancelled(), "协作返回不等于Future已取消")
finally:
    停止.set()
    池.shutdown(wait=True)
检查(not 排队已跑.is_set(), "已取消任务未执行")

# 二:shutdown不等待,只取消尚未开始的任务(Python 3.9+)
启动, 放行, 排队已跑 = Event(), Event(), Event()
池 = ThreadPoolExecutor(max_workers=1)
try:
    运行 = 池.submit(工作, 启动, 放行)
    会合(启动)
    排队 = 池.submit(排队已跑.set)
    池.shutdown(wait=False, cancel_futures=True)
    检查(运行.running() and not 运行.done(), "shutdown已返回,运行任务仍在")
    检查(排队.cancelled(), "shutdown取消排队任务")
    检查(应抛(RuntimeError, lambda: 池.submit(str)), "shutdown后拒绝提交")
finally:
    放行.set()
    池.shutdown(wait=True)
检查(not 排队已跑.is_set(), "shutdown取消的任务未执行")

# 三:在独立控制线程中退出with,记录公开shutdown入口
启动, 放行, 退出入口, 已退出 = Event(), Event(), Event(), Event()
共享, 错误 = [], []
class 观察池(ThreadPoolExecutor):
    def shutdown(self, wait=True, *, cancel_futures=False):
        共享.append(wait)
        退出入口.set()
        return super().shutdown(wait=wait, cancel_futures=cancel_futures)

def 控制():
    try:
        with 观察池(max_workers=1) as 池:
            共享.append(池.submit(工作, 启动, 放行))
            会合(启动)
        已退出.set()
    except BaseException as 异常:
        错误.append(异常)
        退出入口.set()

控制线程 = Thread(target=控制)
控制线程.start()
try:
    会合(退出入口)
    if 错误:
        raise 错误[0]
    检查(共享[1] is True, "with调用shutdown默认wait=True")
    检查(共享[0].running() and not 已退出.is_set(), "未放行时with尚未退出")
finally:
    放行.set()
    控制线程.join(15)
检查(not 控制线程.is_alive() and not 错误 and 已退出.is_set(), "放行后with退出,控制线程已回收")

# 四:结束不等于成功,业务也能抛TimeoutError
业务异常 = TimeoutError("业务主动报超时")
def 失败():
    raise 业务异常

with ThreadPoolExecutor(max_workers=1) as 池:
    正常 = 池.submit(lambda: "完成")
    异常结果 = 池.submit(失败)
检查(正常.done() and 正常.result() == "完成", "正常任务返回结果")
检查(异常结果.done() and not 异常结果.cancelled(), "业务异常也是done")
检查(异常结果.exception() is 业务异常, "exception保留业务异常")
检查(应抛(TimeoutError, 异常结果.result), "已结束任务result仍重抛业务TimeoutError")
print(f"合计:{次数}项检查;Python {sys.version.split()[0]};优化级别{sys.flags.optimize}")

四、结果怎么看:协作停止不是取消成功

本机执行中,等待超时之后running仍为True、done为False,运行任务的cancel返回False。排队任务则取消成功,result抛CancelledError,并且它的执行标记始终没有被设置。这里是用会合点控制状态,不是靠短暂sleep猜线程有没有启动。

停止信号被设置后,工作函数自己返回,Future正常完成而非cancelled。这就是协作停止:任务在自己安排的检查点响应信号。示例只有一个等待点;真正的分段计算要在合适边界检查Event,阻塞调用也需要自己的超时或退出机制。仅仅把Event设成True,不会神奇地打断任意函数。

五、shutdown的两个开关,管的不是一件事

wait控制shutdown是否等待任务收尾;cancel_futures控制是否取消尚未开始运行的任务,这个参数从Python 3.9加入。两者不要混读成“立即杀掉全部线程”。实验先占住唯一worker,再提交一个排队任务,调用不等待并取消排队的shutdown后,前者仍在运行,后者已取消。

shutdown之后再submit会报RuntimeError,不能继续往同一个池里补任务。wait=False只是当前方法不等,并不承诺Python程序立刻退出;文档明确说明程序仍要等待未完成的任务。实验最后主动放行,并再次等待收尾,不把遗留线程扔给解释器退出阶段。

六、为什么捕获超时后,with还是没出来

with退出时按wait=True执行shutdown。所以代码可能已经捕获result的等待超时,却在退出上下文时继续等待工作函数结束。这不是超时失效,而是取结果与回收线程池两个独立步骤:前者不等了,后者仍要收尾。

示例把with放在独立控制线程里,用子类记录公开shutdown入口收到的wait参数。主线程看到入口信号后,检查任务仍运行、上下文尚未退出,再放行任务并join。入口信号不是对内部锁的窥探;它和受控任务、最终退出事件一起,验证本例的调用参数与先后关系,不测量退出耗时。

七、别只凭TimeoutError,就决定重试

还有个容易漏掉的同名异常:工作函数自己也能抛TimeoutError。result会重抛它,不能只凭异常类型就断言是等待期限用完了。示例最后让业务函数主动抛出一个异常对象,先等线程池结束,再核对done和exception,确认此时拿到的是业务异常。

超时结果未知时,不要无脑再提交一次有副作用的任务。应先通过任务标识查询最终状态,写入类操作还要考虑幂等和去重。线程退出、Future取消和业务补偿是不同层面的事;这个本地实验没有证明任何真实请求未发送、事务已撤销或重试一定安全。即使调用方已经放弃等待,也应给任务保留可追踪的去向,而不是只留一条超时日志。

八、落地前按这份清单验收

默认、-O和-OO三种模式分别真实运行同一脚本,每次都完成二十项显式检查且退出码为零。三次重放不是六十个独立用例。文章HTML中的完整代码还会被抽出,逐字节核对源脚本后再运行,防止转义或排版悄悄改坏示例。

  • 区分等待超时、取消成功、协作返回和业务异常。
  • 用启动与释放事件确认运行状态,不靠sleep断言时序。
  • 验证排队任务没有执行,退出时释放会合点并回收线程。
  • 确认Python版本;cancel_futures需要3.9及以上。
  • 有副作用时先确认结果与幂等条件,不拿本实验代替业务验收。

参考资料只支撑相应API语义;PEP用于了解设计背景,现行参数和版本边界以库文档及对应版本文档为准。环境不同可以重放脚本,但不要把本机3.14.3的隔离记录写成生产系统验证结果。

  1. Python 3.14 concurrent.futures文档
  2. CPython v3.14.3对应文档原文
  3. PEP 3148:futures设计

🔕 评论已关闭