无所不能的Python竟然没有一个像样的定时器?试试这个!

开发 后端
所谓定时器,是指间隔特定时间执行特定任务的机制。几乎所有的编程语言,都有定时器的实现。比如,Java有util.Timer和util.TimerTask,JavaScript有setInterval和setTimeout,可以实现非常复杂的定时任务处理。

[[414409]]

本文转载自微信公众号「Python作业辅导员」,作者天元浪子。转载本文请联系Python作业辅导员公众号。

所谓定时器,是指间隔特定时间执行特定任务的机制。几乎所有的编程语言,都有定时器的实现。比如,Java有util.Timer和util.TimerTask,JavaScript有setInterval和setTimeout,可以实现非常复杂的定时任务处理。然而,牛叉到无所不能的Python,却没有一个像样的定时器,实在令人难以理解。

刚入门的同学一定会说:不是有个time.sleep吗?定好闹钟睡大觉,闹钟一响,起来干活,这不就是一个定时器吗?没错,time.sleep具备定时器的基本要素,但若作为定时器使用,则有两个致命的缺陷:一是阻塞主线程,睡觉的时候不能做任何事情;二是醒来以后需要主线程执行定时任务——即便使用线程技术,也得先由主线程来创建子线程。

说到这里,熟悉线程模块threading的同学也许会说:threading.Timer就是以线程方式运行的呀,既不会阻塞主线程,执行定时任务也无需主线程干预,这不就是一个完美的定时器吗?

我们先来看看threading.Timer是如何工作的。下面这段代码演示了threading.Timer的基本用法:启动定时器2秒钟后以线程方式调用函数do_something,在定时器等待的2秒钟内,以及do_something运行期间,主线程仍然可以做其他工作——此处是从键盘读取输入,借以阻塞主线程,以便观察定时器的工作情况。

  1. import time 
  2. import threading 
  3.  
  4. def do_something(name, gender='male'): 
  5.     print(time.time(), '定时时间到,执行特定任务' ) 
  6.     print('name:%s, gender:%s'%(name, gender)) 
  7.  
  8. timer = threading.Timer(2, do_something, args=('Alice',), kwargs={'gender':'female'}) 
  9. timer.start() 
  10. print(time.time(), '定时开始时间'
  11. input('按回车键结束\n') # 此处阻塞主进程 

正如我们所期待的那样,定时器启动2秒钟后,函数do_something被调用,这期间可以随时敲击回车键结束程序。这段代码的运行结果如下。

  1. 1627438957.4297626 定时开始时间 
  2. 按回车键结束 
  3. 1627438959.4299397 定时时间到,执行特定任务 
  4. name:Alice, gender:female 

从使用效果看,threading.Timer称得上是一款简洁易用的定时器。不过,threading.Timer存在明显的短板,那就是不支持连续的定时任务,比如,每隔2秒钟调用一次do_something函数。如果一定要用threading.Timer实现连续定时,只能用类似嵌套的变通方法,在do_something函数中再次启动定时器。

  1. import time 
  2. import threading 
  3.  
  4. def do_something(name, gender='male'): 
  5.     global timer 
  6.     timer = threading.Timer(2, do_something, args=(name,), kwargs={'gender':gender}) 
  7.     timer.start() 
  8.  
  9.     print(time.time(), '定时时间到,执行特定任务' ) 
  10.     print('name:%s, gender:%s'%(name, gender)) 
  11.     time.sleep(5) 
  12.     print(time.time(), '完成特定任务' ) 
  13.  
  14. timer = threading.Timer(2, do_something, args=('Alice',), kwargs={'gender':'female'}) 
  15. timer.start() 
  16. input('按回车键结束\n') # 此处阻塞主进程 

这段代码重新定义了do_something函数,在函数开始位置启动下一次的定时任务。之所以放在开始位置,是为了保证两次定时之间的时间间隔尽可能精确。饶是如此,下面的运行结果显示,两次定时之间的时间间隔比设计的2秒钟多了大约10毫秒,且误差是连续累计的,重复执行100次,误差将会超过1秒钟。

  1. 按回车键结束 
  2. 1627440628.683803 定时时间到,执行特定任务 
  3. name:Alice, gender:female 
  4. 1627440630.6929214 定时时间到,执行特定任务 
  5. name:Alice, gender:female 
  6. 1627440632.707388 定时时间到,执行特定任务 
  7. name:Alice, gender:female 
  8. 1627440633.6890671 完成特定任务 
  9. 1627440634.722474 定时时间到,执行特定任务 
  10. name:Alice, gender:female 
  11. 1627440635.7092102 完成特定任务 
  12. 1627440636.7277966 定时时间到,执行特定任务 
  13. name:Alice, gender:female 

针对连续的定时任务,threading.Timer的表现还算差强人意,只是这种嵌套的写法完全颠覆了代码美学。对于像我这样有代码洁癖的程序员来说,是无法容忍和不可接受的。在我看来,一个完美的定时器应该满足以下5个条件,具备下图所示的结构。

  1. 不阻塞主线程
  2. 同时支持单次定时和连续定时
  3. 以线程或进程方式执行定时任务
  4. 定时任务的线程或进程的创建、运行,不影响定时精度
  5. 足够精确的定时精度,且误差不会累计

既然Python没有提供一个像样的定时器,那就自己写一个吧。下面这个定时器,满足上面提到的5个条件,最短时间间隔可以低至10毫秒,且误差不会累计。虽然还不够完美,但无论结构还是精度,都还说得过去。

  1. import time 
  2. import threading 
  3.  
  4. class PyTimer: 
  5.     """定时器类""" 
  6.  
  7.     def __init__(self, func, *args, **kwargs): 
  8.         """构造函数""" 
  9.  
  10.         self.func = func 
  11.         self.args = args 
  12.         self.kwargs = kwargs 
  13.         self.running = False 
  14.  
  15.     def _run_func(self): 
  16.         """运行定时事件函数""" 
  17.  
  18.         th = threading.Thread(target=self.func, args=self.args, kwargs=self.kwargs) 
  19.         th.setDaemon(True
  20.         th.start() 
  21.  
  22.     def _start(self, interval, once): 
  23.         """启动定时器的线程函数""" 
  24.  
  25.         if interval < 0.010: 
  26.             interval = 0.010 
  27.  
  28.         if interval < 0.050: 
  29.             dt = interval/10 
  30.         else
  31.             dt = 0.005 
  32.  
  33.         if once: 
  34.             deadline = time.time() + interval 
  35.             while time.time() < deadline: 
  36.                 time.sleep(dt) 
  37.  
  38.             # 定时时间到,调用定时事件函数 
  39.             self._run_func() 
  40.         else
  41.             self.running = True 
  42.             deadline = time.time() + interval 
  43.             while self.running: 
  44.                 while time.time() < deadline: 
  45.                     time.sleep(dt) 
  46.  
  47.                 # 更新下一次定时时间 
  48.                 deadline += interval 
  49.  
  50.                 # 定时时间到,调用定时事件函数 
  51.                 if self.running: 
  52.                     self._run_func() 
  53.  
  54.     def start(self, interval, once=False): 
  55.         """启动定时器 
  56.  
  57.         interval    - 定时间隔,浮点型,以秒为单位,最高精度10毫秒 
  58.         once        - 是否仅启动一次,默认是连续的 
  59.         ""
  60.  
  61.         th = threading.Thread(target=self._start, args=(interval, once)) 
  62.         th.setDaemon(True
  63.         th.start() 
  64.  
  65.     def stop(self): 
  66.         """停止定时器""" 
  67.  
  68.         self.running = False 

定时器类PyTimer实例化时,需要传入定时任务函数。如果定时任务函数有参数,也可以按照位置参数、关键字参数的顺序一并提供。PyTimer定时器提供start和stop两个方法,用于启动和停止定时器。其中stop方法不需要参数,start则需要一个以秒为单位的定时间隔参数。start还有一个布尔型的默认参数once,可以设置是否单次定时。once参数的默认值为False,即默认连续定时;如果需要单次定时,只需要将once置为true即可。

  1. def do_something(name, gender='male'): 
  2.     print(time.time(), '定时时间到,执行特定任务' ) 
  3.     print('name:%s, gender:%s'%(name, gender)) 
  4.     time.sleep(5) 
  5.     print(time.time(), '完成特定任务' ) 
  6.  
  7. timer = PyTimer(do_something, 'Alice', gender='female'
  8. timer.start(0.5, once=False
  9.  
  10. input('按回车键结束\n') # 此处阻塞主进程 
  11. timer.stop() 

上面是使用PyTimer定时器以0.5秒钟的间隔连续调用函数do_something的例子。这段代码的运行结果如下。

  1. 按回车键结束 
  2. 1627450313.425347 定时时间到,执行特定任务 
  3. name:Alice, gender:female 
  4. 1627450313.9226055 定时时间到,执行特定任务 
  5. name:Alice, gender:female 
  6. 1627450314.421761 定时时间到,执行特定任务 
  7. name:Alice, gender:female 
  8. 1627450314.9243422 定时时间到,执行特定任务 
  9. name:Alice, gender:female 
  10. 1627450315.422722 定时时间到,执行特定任务 
  11. name:Alice, gender:female 
  12. 1627450315.9200313 定时时间到,执行特定任务 
  13. name:Alice, gender:female 
  14. 1627450316.4204514 定时时间到,执行特定任务 
  15. name:Alice, gender:female 
  16. 1627450316.9215539 定时时间到,执行特定任务 
  17. name:Alice, gender:female 
  18. 1627450317.4228196 定时时间到,执行特定任务 
  19. name:Alice, gender:female 
  20. 1627450317.9245899 定时时间到,执行特定任务 
  21. name:Alice, gender:female 
  22. 1627450318.42355 定时时间到,执行特定任务 
  23. name:Alice, gender:female 
  24. 1627450318.4393418 完成特定任务 
  25. 1627450318.9251466 定时时间到,执行特定任务 
  26. name:Alice, gender:female 
  27. 1627450318.9395308 完成特定任务 
  28. 1627450319.4242043 完成特定任务 
  29. 1627450319.4242043 定时时间到,执行特定任务 
  30. name:Alice, gender:female 
  31. 1627450319.9253905 定时时间到,执行特定任务 
  32. name:Alice, gender:female 
  33. 1627450319.9411068 完成特定任务 
  34. 1627450320.425871 完成特定任务 
  35. 1627450320.425871 定时时间到,执行特定任务 
  36. name:Alice, gender:female 
虽然每个定时任务需要运行5秒钟,但每隔0.5秒都会准时启动一个新的线程运行定时任务。从记录可以看出,尽管每次定时任务的启动时间有几个毫秒的误差,但误差不会累计,重复执行的时间间隔均值始终稳定在0.5秒。
 
【责任编辑:武晓燕 TEL:(010)68476606】

 

责任编辑:武晓燕 来源: Python作业辅导员
相关推荐

2013-05-22 14:05:18

2021-04-07 10:02:18

人工智能技术互联网

2009-07-06 18:43:13

VB指针

2021-04-17 23:05:29

手机黑科技APP

2023-11-28 12:09:43

Linux网络工具

2015-04-30 14:53:43

Build 2015微软Azure

2021-04-20 10:47:59

手机APP科技

2021-08-16 10:13:26

AI DeepMind人工智能

2013-12-18 10:32:20

mcollectiveshellpuppet

2014-06-13 02:21:14

2010-07-28 15:56:22

FlexTimer定时

2009-11-11 10:14:10

linux定时器操作系统

2021-04-05 22:37:29

微信小程序手机

2015-10-28 15:47:22

谷歌物联网互联网

2014-03-11 23:02:00

2021-04-01 13:07:07

编码开发代码

2011-09-12 15:32:58

2022-11-02 11:40:16

Flowable定时器流程

2022-09-26 08:26:38

软件定时器函数

2019-12-19 14:17:11

以太坊去中心化加密货币
点赞
收藏

51CTO技术栈公众号