Python sched 取消事件:从队列移除了任务,为什么再次 cancel 会报错
程序保存了定时事件,用户取消后从队列移除。清理代码再次取消同一事件,却收到 ValueError。sched.cancel 的契约是移除仍在队列里的那个事件,不是一个重复调用也永远成功的“保证不存在”操作。已经取消或已被取出执行的事件,都不能再按原来的待执行状态处理。
调试调度器不必每次真的等五秒。sched 允许传入取时函数和延迟函数,下面让时间只是一项从一百开始的整数,延迟仅给它加数。保存完整代码为 demo.py,执行 python demo.py 即可;没有真实睡眠、网络访问或后台循环,四个预先安排的事件让运行必定结束。
AI生成概念插图:时间线中一个事件块被移出,旁边的时钟表示调度时间可独立推进;不是软件界面或运行截图。
import sched
class Clock:
def __init__(self):
self.now = 100
def time(self):
return self.now
def delay(self, amount):
assert amount >= 0
self.now += amount
clock = Clock()
s = sched.scheduler(clock.time, clock.delay)
seen = []
def record(name):
seen.append((clock.now, name))
s.enterabs(102, 2, record, argument=("second",))
s.enterabs(102, 1, record, argument=("first",))
removed = s.enterabs(103, 1, record, argument=("removed",))
s.enterabs(105, 1, record, argument=("last",))
s.cancel(removed)
try:
s.cancel(removed)
except ValueError:
print("repeat cancel: ValueError")
else:
raise AssertionError("cancel accepted absent event")
remaining = s.run(blocking=False)
assert remaining == 2 and clock.now == 100 and seen == []
print("next delay:", remaining)
clock.delay(2)
assert s.run(blocking=False) == 3
assert seen == [(102, "first"), (102, "second")]
s.run()
assert seen == [(102, "first"), (102, "second"), (105, "last")]
assert s.empty() and clock.now == 105
print("executed:", seen)取消需要入队时返回的句柄
removed 保存的是 enterabs 返回的事件对象,cancel 用它从队列移除那一项。第二次调用捕获 ValueError,并打印 repeat cancel: ValueError。这条异常说明调用时它已不在队列中,单凭异常本身不能推断“刚才一定被用户取消”,因为已经开始执行也可能导致同样的结果。
如果业务要求取消操作可重复,应在包装层明确记录状态,并决定遇到已执行任务时怎样向用户说明。不要无条件忽略所有异常,再声称动作已经被阻止。对于产生外部影响的回调,还需要它自己的取消检查;从队列删除一个尚未执行的事件,不能撤回已经完成的发送或写入。
同一时刻再按优先级排队
一百零二时刻先入队的是 second,后入队的是 first,但 first 的优先级数值为一,更小,所以它先执行。时间决定先到哪个时刻,优先级用于相同时刻的事件。若两个任务时间不同,不能指望更高优先级让未来的事件越过尚未到来的时刻。
首次非阻塞 run 时没有到期事件,时钟仍是一百,记录列表为空。在本例的 CPython 实现中返回二,即距离下一事件的剩余间隔;官方源码这里返回 time 减 now。不要把这个返回值直接当作绝对时刻一百零二。手动推进二之后再次运行,处理两项到期事件,并返回下一项还需等待三。
最后一次普通 run 通过我们提供的 delay 把虚拟时间推进到一百零五,执行 last 后队列为空。调度器在回调后还会调用一次零延迟,所以模拟时钟也必须接受零,不能把每一次 delay 调用都误当作真实时间流逝。最终列表里没有 removed,取消效果通过完整执行轨迹得到确认。
虚拟时钟让时间推进可控,却不会自动模拟线程竞态、系统时钟变化或回调实际耗时。本例的回调只是追加内存记录,因此可精确断言时间。实际回调如果耗时超过排期,调度器会落后而不是自动丢弃事件;需要跳过过期任务时,应由业务明确判断并取消仍无意义的待执行项。
资料核对日期:2026年10月2日(北京时间)。示例在本地CPython 3.12.14实际运行并通过断言,结果对应文中固定输入。


