关于计时器:在Python中每x秒重复执行一次函数的最佳方法是什么?

What is the best way to repeatedly execute a function every x seconds in Python?

我想永远每60秒在Python中重复执行一个函数(就像目标C中的NSTimer一样)。 这段代码将作为守护进程运行,实际上就像使用cron每分钟调用python脚本一样,但不需要用户设置。

在这个关于用Python实现的cron的问题中,解决方案似乎实际上只是sleep()x秒。 我不需要这样的高级功能,所以也许这样的东西可行

1
2
3
while True:
    # Code executed here
    time.sleep(60)

这段代码有可预见的问题吗?


使用sched模块,它实现了一个通用的事件调度程序。

1
2
3
4
5
6
7
8
9
import sched, time
s = sched.scheduler(time.time, time.sleep)
def do_something(sc):
    print"Doing stuff..."
    # do your stuff
    s.enter(60, 1, do_something, (sc,))

s.enter(60, 1, do_something, (s,))
s.run()


只需将时间循环锁定到系统时钟即可。简单。

1
2
3
4
5
import time
starttime=time.time()
while True:
  print"tick"
  time.sleep(60.0 - ((time.time() - starttime) % 60.0))


您可能想要考虑Twisted,它是一个实现Reactor Pattern的Python网络库。

1
2
3
4
5
6
7
8
9
10
11
12
from twisted.internet import task, reactor

timeout = 60.0 # Sixty seconds

def doWork():
    #do work here
    pass

l = task.LoopingCall(doWork)
l.start(timeout) # call every sixty seconds

reactor.run()

虽然"while True:sleep(60)"可能会工作Twisted可能已经实现了你最终需要的许多功能(如bobince所指出的守护进程,日志记录或异常处理),并且可能是一个更强大的解决方案


如果你想要一种非阻塞方式来定期执行你的函数,而不是阻塞无限循环,我会使用一个线程计时器。这样,您的代码可以继续运行并执行其他任务,并且每隔n秒仍然会调用您的函数。我在很长的CPU /磁盘/网络密集型任务中使用这种技术来打印进度信息。

这是我在类似问题中发布的代码,包含start()和stop()控件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from threading import Timer

class RepeatedTimer(object):
    def __init__(self, interval, function, *args, **kwargs):
        self._timer     = None
        self.interval   = interval
        self.function   = function
        self.args       = args
        self.kwargs     = kwargs
        self.is_running = False
        self.start()

    def _run(self):
        self.is_running = False
        self.start()
        self.function(*self.args, **self.kwargs)

    def start(self):
        if not self.is_running:
            self._timer = Timer(self.interval, self._run)
            self._timer.start()
            self.is_running = True

    def stop(self):
        self._timer.cancel()
        self.is_running = False

用法:

1
2
3
4
5
6
7
8
9
10
11
from time import sleep

def hello(name):
    print"Hello %s!" % name

print"starting..."
rt = RepeatedTimer(1, hello,"World") # it auto-starts, no need of rt.start()
try:
    sleep(5) # your long-running job goes here...
finally:
    rt.stop() # better in a try/finally block to make sure the program ends!

特征:

  • 仅限标准库,无外部依赖项
  • 即使定时器已经启动/停止,start()stop()也可以安全地多次调用
  • 要调用的函数可以有位置和命名参数
  • 您可以随时更改interval,它将在下次运行后生效。 argskwargs甚至function也是如此!


我认为更简单的方法是:

1
2
3
4
5
6
7
8
import time

def executeSomething():
    #code here
    time.sleep(60)

while True:
    executeSomething()

这样你的代码就会被执行,然后等待60秒然后它再次执行,等待,执行等......
无需复杂化:D


以下是MestreLion代码的更新,可以避免随着时间的推移而进行漫游:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import threading
import time

class RepeatedTimer(object):
  def __init__(self, interval, function, *args, **kwargs):
    self._timer = None
    self.interval = interval
    self.function = function
    self.args = args
    self.kwargs = kwargs
    self.is_running = False
    self.next_call = time.time()
    self.start()

  def _run(self):
    self.is_running = False
    self.start()
    self.function(*self.args, **self.kwargs)

  def start(self):
    if not self.is_running:
      self.next_call += self.interval
      self._timer = threading.Timer(self.next_call - time.time(), self._run)
      self._timer.start()
      self.is_running = True

  def stop(self):
    self._timer.cancel()
    self.is_running = False


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import time, traceback

def every(delay, task):
  next_time = time.time() + delay
  while True:
    time.sleep(max(0, next_time - time.time()))
    try:
      task()
    except Exception:
      traceback.print_exc()
      # in production code you might want to have this instead of course:
      # logger.exception("Problem while executing repetitive task.")
    # skip tasks if we are behind schedule:
    next_time += (time.time() - next_time) // delay * delay + delay

def foo():
  print("foo", time.time())

every(5, foo)

如果你想在不阻塞剩余代码的情况下执行此操作,可以使用它来让它在自己的线程中运行:

1
2
import threading
threading.Thread(target=lambda: every(5, foo)).start()

该解决方案结合了其他解决方案中很少结合的几种功能:

  • 异常处理:在此级别上尽可能正确处理异常,即:即记录以进行调试,而不会中止我们的程序。
  • 没有链接:你在许多答案中找到的常见的类链实现(用于调度下一个事件)在调度机制(threading.Timer或其他)中出现任何错误的方面是脆弱的,这将终止链。即使问题的原因已经解决,也不会再发生进一步的执行。与简单的sleep()相比,一个简单的循环更加稳健。
  • 没有漂移:我的解决方案可以准确跟踪它应该运行的时间。根据执行时间没有漂移(如许多其他解决方案中那样)。
  • 跳过:如果一次执行耗费太多时间,我的解决方案将跳过任务(例如,每五秒执行一次X,但X需要6秒)。这是标准的cron行为(并且有充分的理由)。然后,许多其他解决方案只是连续几次执行任务而没有任何延迟。对于大多数情况(例如清理任务),这是不希望的。如果需要,只需使用next_time += delay


一段时间后我遇到了类似的问题。可能是http://cronus.readthedocs.org可能有帮助吗?

对于v0.2,以下代码段有效

1
2
3
4
5
6
import cronus.beat as beat

beat.set_rate(2) # 2 Hz
while beat.true():
    # do some time consuming work here
    beat.sleep() # total loop duration would be 0.5 sec

它与cron之间的主要区别在于异常将终止该守护进程。您可能希望使用异常捕获器和记录器进行换行。


这是MestreLion代码的改编版本。
除了原始函数,这段代码:

1)添加first_interval用于在特定时间触发定时器(调用者需要计算first_interval并传入)

2)用原始代码解决竞争条件。在原始代码中,如果控制线程未能取消正在运行的计时器("停止计时器,并取消执行计时器的操作。这只有在计时器仍处于等待阶段时才有效。"引自https:// docs.python.org/2/library/threading.html),计时器将无休止地运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class RepeatedTimer(object):
def __init__(self, first_interval, interval, func, *args, **kwargs):
    self.timer      = None
    self.first_interval = first_interval
    self.interval   = interval
    self.func   = func
    self.args       = args
    self.kwargs     = kwargs
    self.running = False
    self.is_started = False

def first_start(self):
    try:
        # no race-condition here because only control thread will call this method
        # if already started will not start again
        if not self.is_started:
            self.is_started = True
            self.timer = Timer(self.first_interval, self.run)
            self.running = True
            self.timer.start()
    except Exception as e:
        log_print(syslog.LOG_ERR,"timer first_start failed %s %s"%(e.message, traceback.format_exc()))
        raise

def run(self):
    # if not stopped start again
    if self.running:
        self.timer = Timer(self.interval, self.run)
        self.timer.start()
    self.func(*self.args, **self.kwargs)

def stop(self):
    # cancel current timer in case failed it's still OK
    # if already stopped doesn't matter to stop again
    if self.timer:
        self.timer.cancel()
    self.running = False

一个可能的答案:

1
2
3
4
5
6
7
import time
t=time.time()

while True:
    if time.time()-t>10:
        #run your task here
        t=time.time()


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    ''' tracking number of times it prints'''
import threading

global timeInterval
count=0
def printit():
  threading.Timer(timeInterval, printit).start()
  print("Hello, World!")
  global count
  count=count+1
  print(count)
printit

if __name__ =="__main__":
    timeInterval= int(input('Enter Time in Seconds:'))
    printit()


我使用Tkinter after()方法,它不会"窃取游戏"(就像之前介绍的sched模块一样),即它允许其他东西并行运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import Tkinter

def do_something1():
  global n1
  n1 += 1
  if n1 == 6: # (Optional condition)
    print"* do_something1() is done *"; return
  # Do your stuff here
  # ...
  print"do_something1()"+str(n1)
  tk.after(1000, do_something1)

def do_something2():
  global n2
  n2 += 1
  if n2 == 6: # (Optional condition)
    print"* do_something2() is done *"; return
  # Do your stuff here
  # ...
  print"do_something2()"+str(n2)
  tk.after(500, do_something2)

tk = Tkinter.Tk();
n1 = 0; n2 = 0
do_something1()
do_something2()
tk.mainloop()

do_something1()do_something2()可以并行运行,也可以以任何间隔速度运行。在这里,第二个将执行两倍快。还注意我使用一个简单的计数器作为终止任一功能的条件。您可以使用您喜欢的任何其他部分,或者如果您在程序终止之前运行的功能(例如时钟),则可以使用任何其他部分。


例如,显示当前本地时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import datetime
import glib
import logger

def get_local_time():
    current_time = datetime.datetime.now().strftime("%H:%M")
    logger.info("get_local_time(): %s",current_time)
    return str(current_time)

def display_local_time():
    logger.info("Current time is: %s", get_local_time())
    return True

# call every minute
glib.timeout_add(60*1000, display_local_time)

我使用它来导致每小时60个事件,大多数事件发生在整个分钟后的相同秒数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import math
import time
import random

TICK = 60 # one minute tick size
TICK_TIMING = 59 # execute on 59th second of the tick
TICK_MINIMUM = 30 # minimum catch up tick size when lagging

def set_timing():

    now = time.time()
    elapsed = now - info['begin']
    minutes = math.floor(elapsed/TICK)
    tick_elapsed = now - info['completion_time']
    if (info['tick']+1) > minutes:
        wait = max(0,(TICK_TIMING-(time.time() % TICK)))
        print ('standard wait: %.2f' % wait)
        time.sleep(wait)
    elif tick_elapsed < TICK_MINIMUM:
        wait = TICK_MINIMUM-tick_elapsed
        print ('minimum wait: %.2f' % wait)
        time.sleep(wait)
    else:
        print ('skip set_timing(); no wait')
    drift = ((time.time() - info['begin']) - info['tick']*TICK -
        TICK_TIMING + info['begin']%TICK)
    print ('drift: %.6f' % drift)

info['tick'] = 0
info['begin'] = time.time()
info['completion_time'] = info['begin'] - TICK

while 1:

    set_timing()

    print('hello world')

    #random real world event
    time.sleep(random.random()*TICK_MINIMUM)

    info['tick'] += 1
    info['completion_time'] = time.time()

根据实际情况,您可能会得到长度的标记:

1
60,60,62,58,60,60,120,30,30,60,60,60,60,60...etc.

但在60分钟结束时,你将有60个蜱虫;并且它们中的大多数将以正确的偏移量发生到您喜欢的那一分钟。

在我的系统上,我得到典型的漂移<1/20秒,直到需要进行校正。

这种方法的优点是时钟漂移的分辨率;如果您正在执行诸如每个刻度附加一个项目并且您希望每小时附加60个项目,这可能会导致问题。如果不考虑漂移,可能会导致移动平均等次要指示将数据过深地考虑在过去,从而导致输出错误。