Python定时器深度解析:从threading.Timer缺陷到高精度PyTimer实现
本文深入分析Python标准库中threading.Timer在连续定时任务中的误差累计缺陷,并介绍一种自定义PyTimer类。该实现支持单次与连续定时,采用多线程非阻塞执行,通过动态调整休眠时间将误差控制在10毫秒级别且不累计,满足高精度定时需求。
Python定时器现状与基础实现
定时器是一种按固定间隔执行特定任务的机制,几乎每种编程语言都提供了相应的实现。例如,Java提供了util.Timer和util.TimerTask,JavaScript拥有setInterval和setTimeout,它们都能处理复杂的定时逻辑。然而,功能强大的Python却缺乏一个直观且完善的内置定时器,这显得有些出人意料。
初学者可能会提到time.sleep函数,认为设置好时间后等待即可,这看似具备定时器的基本特征。但实际上,time.sleep存在两个主要局限:首先,它会阻塞主线程,导致在等待期间无法执行其他操作;其次,即使借助线程技术,也需要主线程先创建子线程来执行任务,增加了复杂性。
熟悉threading模块的开发者可能会指出threading.Timer,它通过线程运行,不阻塞主线程且无需主线程干预即可执行任务,看似是一个理想的解决方案。
让我们先了解threading.Timer的工作机制。以下代码演示了其基本用法:启动一个定时器,2秒后以线程方式调用do_something函数。在等待的2秒内以及do_something执行期间,主线程仍可执行其他操作——此处通过键盘输入阻塞主线程,以便观察定时器的行为。
import time
import threading
def do_something(name, gender='male'):
print(time.time(), '定时时间到,执行特定任务' )
print('name:%s, gender:%s'%(name, gender))
timer = threading.Timer(2, do_something, args=('Alice',), kwargs={'gender':'female'})
timer.start()
print(time.time(), '定时开始时间')
input('按回车键结束\n') # 此处阻塞住进程
如预期所示,定时器在启动2秒后调用do_something函数,期间可随时按回车键结束程序。运行结果如下。
1627438957.4297626 定时开始时间
按回车键结束
1627438959.4299397 定时时间到,执行特定任务
name:Alice, gender:female
threading.Timer的误差累计问题
从使用效果来看,threading.Timer简洁易用。但其明显短板在于不支持连续的定时任务,例如每隔2秒调用一次do_something函数。若强行使用threading.Timer实现连续定时,只能采用嵌套方式,即在do_something函数内部再次启动定时器。
import time
import threading
def do_something(name, gender='male'):
global timer
timer = threading.Timer(2, do_something, args=(name,), kwargs={'gender':gender})
timer.start()
print(time.time(), '定时时间到,执行特定任务' )
print('name:%s, gender:%s'%(name, gender))
time.sleep(5)
print(time.time(), '完成特定任务' )
timer = threading.Timer(2, do_something, args=('Alice',), kwargs={'gender':'female'})
timer.start()
input('按回车键结束\n') # 此处阻塞住进程
上述代码重新定义了do_something函数,在函数起始处启动下一次定时任务。将其置于起始位置是为了尽量保证两次定时之间的时间间隔精确。然而,运行结果显示,两次定时之间的间隔比设定的2秒多了约10毫秒,且误差会连续累计。重复执行100次后,误差可能超过1秒。
按回车键结束
1627440628.683803 定时时间到,执行特定任务
name:Alice, gender:female
1627440630.6929214 定时时间到,执行特定任务
name:Alice, gender:female
1627440632.707388 定时时间到,执行特定任务
name:Alice, gender:female
1627440633.6890671 完成特定任务
1627440634.722474 定时时间到,执行特定任务
name:Alice, gender:female
1627440635.7092102 完成特定任务
1627440636.7277966 定时时间到,执行特定任务
name:Alice, gender:female
理想定时器的设计标准
对于连续定时任务,threading.Timer的表现尚可,但这种嵌套写法严重破坏了代码的美学。对于追求代码整洁的开发者而言,这种实现方式难以接受。一个理想的定时器应满足以下5个条件,并具备相应的结构。
- 不阻塞主线程
- 同时支持单次定时和连续定时
- 以线程或进程方式执行定时任务
- 定时任务的线程或进程的创建、运行,不影响定时精度
- 足够精确的定时精度,且误差不会累计

高精度PyTimer的实现与测试
既然Python未提供完善的定时器,我们可以自行实现。以下定时器满足上述5个条件,最短时间间隔可低至10毫秒,且误差不会累计。虽然并非完美,但在结构和精度上已相当不错。
import time
import threading
class PyTimer:
"""定时器类"""
def __init__(self, func, *args, **kwargs):
"""构造函数"""
self.func = func
self.args = args
self.kwargs = kwargs
self.running = False
def _run_func(self):
"""运行定时事件函数"""
th = threading.Thread(target=self.func, args=self.args, kwargs=self.kwargs)
th.setDaemon(True)
th.start()
def _start(self, interval, once):
"""启动定时器的线程函数"""
if interval < 0.010:
interval = 0.010
if interval < 0.050:
dt = interval/10
else:
dt = 0.005
if once:
deadline = time.time() + interval
while time.time() < deadline:
time.sleep(dt)
# 定时时间到,调用定时事件函数
self._run_func()
else:
self.running = True
deadline = time.time() + interval
while self.running:
while time.time() < deadline:
time.sleep(dt)
# 更新下一次定时时间
deadline += interval
# 定时时间到,调用定时事件函数
if self.running:
self._run_func()
def start(self, interval, once=False):
"""启动定时器
interval - 定时间隔,浮点型,以秒为单位,最高精度10毫秒
once - 是否仅启动一次,默认是连续的
"""
th = threading.Thread(target=self._start, args=(interval, once))
th.setDaemon(True)
th.start()
def stop(self):
"""停止定时器"""
self.running = False
实例化PyTimer类时需传入定时任务函数。若任务函数有参数,可按位置参数和关键字参数的顺序提供。PyTimer提供start和stop两个方法,分别用于启动和停止定时器。stop方法无需参数,start方法需传入以秒为单位的定时间隔参数。start还有一个布尔型默认参数once,用于设置是否单次定时。once默认为False,即默认连续定时;若需单次定时,只需将once设为true。
def do_something(name, gender='male'):
print(time.time(), '定时时间到,执行特定任务' )
print('name:%s, gender:%s'%(name, gender))
time.sleep(5)
print(time.time(), '完成特定任务' )
timer = PyTimer(do_something, 'Alice', gender='female')
timer.start(0.5, once=False)
input('按回车键结束\n') # 此处阻塞住进程
timer.stop()
以上示例展示了使用PyTimer以0.5秒间隔连续调用do_something函数的情况。运行结果如下。
按回车键结束
1627450313.425347 定时时间到,执行特定任务
name:Alice, gender:female
1627450313.9226055 定时时间到,执行特定任务
name:Alice, gender:female
1627450314.421761 定时时间到,执行特定任务
name:Alice, gender:female
1627450314.9243422 定时时间到,执行特定任务
name:Alice, gender:female
1627450315.422722 定时时间到,执行特定任务
name:Alice, gender:female
1627450315.9200313 定时时间到,执行特定任务
name:Alice, gender:female
1627450316.4204514 定时时间到,执行特定任务
name:Alice, gender:female
1627450316.9215539 定时时间到,执行特定任务
name:Alice, gender:female
1627450317.4228196 定时时间到,执行特定任务
name:Alice, gender:female
1627450317.9245899 定时时间到,执行特定任务
name:Alice, gender:female
1627450318.42355 定时时间到,执行特定任务
name:Alice, gender:female
1627450318.4393418 完成特定任务
1627450318.9251466 定时时间到,执行特定任务
name:Alice, gender:female
1627450318.9395308 完成特定任务
1627450319.4242043 完成特定任务
1627450319.4242043 定时时间到,执行特定任务
name:Alice, gender:female
1627450319.9253905 定时时间到,执行特定任务
name:Alice, gender:female
1627450319.9411068 完成特定任务
1627450320.425871 完成特定任务
1627450320.425871 定时时间到,执行特定任务
name:Alice, gender:female
尽管每个定时任务需运行5秒,但每隔0.5秒都会准时启动新线程执行任务。从记录可见,尽管每次启动有数毫秒误差,但误差不会累计,重复执行的时间间隔均值始终稳定在0.5秒。
至此,关于基于Python实现简单定时器的详解已完成。如需了解更多Python定时器相关内容,请查阅本站往期文章或继续浏览后续内容,感谢支持!
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









