python從寫定時器學習Thread
python 如何寫一個定時器,循環定時做某一操作呢?
Timer 對象
from threading import Timer
def hello():
print "hello, world"
t = Timer(10.0, hello)
t.start()
10秒后輸出:
hello, world
重點研究 t = Timer(10.0, hello)
這句代碼,python 提供了一個Timer 對象,它會在指定的時間后執行某一操作;它的完整形式:
class threading.Timer(interval, function, args=[], kwargs={})
interval
是時間間隔,function
是可調用的對象,args
和 kwargs
會作為 function
的參數。
注意:這里只會執行一次 function,而不會一直定時執行,且 Timer 在執行操作的時候會創建一個新的線程。
Timer
在 python2 和 python3 有點區別:
# python2.7
def Timer(*args, **kwargs):
return _Timer(*args, **kwargs)
# python3.7
class Timer(Thread):
pass
在 python3,Timer
是 Thread
的子類;在 python2,_Timer
是 Thread
的子類,而 Timer
只是 _Timer
類的工廠方法。
上面的代碼只會打印一次 hello, world
后退出,那么如何循環間隔打印呢?
粗陋的循環定時器
一種方法是在 function
里繼續注冊一個 Timer,這樣就可以在下一個 interval
繼續執行 function
;
from threading import Timer
def hello():
print "hello, world"
Timer(10.0, hello) .start()
t = Timer(10.0, hello)
t.start()
每隔 10 秒輸出一個 hello, world
。
達到效果了,但是這里面好像有點問題。回到 Timer 本身,它是一個 thread,每次循環間隔操作,系統都要創建一個線程,然后再回收,這對系統來說開銷很大。如果時間間隔 interval 很短,系統會一下子創建很多線程,這些線程很難快速回收,導致系統內存和cpu資源被消耗掉。
所以不提倡在 function 里繼續注冊一個 Timer。
更 pythonic 循環定時器
這里有更 pythonic 的方法:
from threading import _Timer
def hello():
print "hello, world"
class RepeatingTimer(_Timer):
def run(self):
while not self.finished.is_set():
self.function(*self.args, **self.kwargs)
self.finished.wait(self.interval)
t = RepeatingTimer(10.0, hello)
t.start()
重點研究 RepeatingTimer
類,它繼承了 threading._Timer
,但是重寫了父類的 run
方法。這是 Python2 的寫法,python3 中 RepeatingTimer
應該繼承 threading.Timer
。
為什么要重寫 Thread
的 run
方法?
_Timer
是一個 Thread
子類,我們先看看 Thread
類的 run
用法。
from threading import Thread
def hello():
print "hello, world"
# 繼承 Thread
class MyThread(Thread):
# 把要執行的代碼寫到run函數里面 線程在創建后會直接運行run函數
def run(self):
hello()
t = MyThread()
t.start()
Thread 對象的完整定義:
class threading.Thread(group=None, target=None, name=None, args=(), kwargs={})
其中 run
方法代碼:
class Thread(_Verbose):
def run(self):
try:
if self.__target:
self.__target(*self.__args, **self.__kwargs)
finally:
# Avoid a refcycle if the thread is running a function with
# an argument that has a member that points to the thread.
del self.__target, self.__args, self.__kwargs
標准的 run
方法用於執行用戶傳入構造函數的 target
方法。 子類可以重寫 run
方法,把要執行的代碼寫到 run
里面,線程在創建后,用戶調用 start()
方法會運行 run()
方法。
所以 RepeatingTimer
重寫 _Timer
的 run() 方法,可以改變線程的執行體,當我們調用 RepeatingTimer
的 start() 方法時會執行我們重寫的 run() 方法。
再看看 RepeatingTimer 類中的 while not self.finished.is_set()
語句,self.finished.is_set()
直到 True
才會退出循環,定時器才結束。finished
是 threading.Event
對象。一個 Event
對象管理着一個 flag 標志,它能被 set()
方法設置為 True,也能被 clear()
方法設置為 False,調用 wait([timeout])
線程會一直 sleep 到 flag 為 True 或超時時間到達。
我們知道定時器有一個 cancel()
方法可以提前取消操作。它其實是調用 Event.clear()
方法提前讓 wait
方法結束等待,並且判斷在 flag 為 true 的情況下不執行定時器操作。具體的代碼:
class _Timer(Thread):
"""Call a function after a specified number of seconds:
t = Timer(30.0, f, args=[], kwargs={})
t.start()
t.cancel() # stop the timer's action if it's still waiting
"""
def __init__(self, interval, function, args=[], kwargs={}):
Thread.__init__(self)
self.interval = interval
self.function = function
self.args = args
self.kwargs = kwargs
self.finished = Event()
def cancel(self):
"""Stop the timer if it hasn't finished yet"""
self.finished.set()
def run(self):
self.finished.wait(self.interval)
if not self.finished.is_set():
self.function(*self.args, **self.kwargs)
self.finished.set()
所以 RepeatingTimer
的 run 方法會一直執行 while
循環體,在循環體了會執行用戶傳入的 function
對象,並等待指定的時間。當用戶想退出定時器時,只需要調用 cancel
方法,將 flag 置為 True 便不會繼續執行循環體了。這樣便完成了一個還不錯的循環定時器。
FAQ
- 准確定時問題
A:
self.function(*self.args, **self.kwargs)
self.finished.wait(self.interval)
有一個疑問,如果function方法是一個很耗時的過程,那么其實並沒有實現准確的定時。比如function完成需要2s,interval設置10s,實際的定時間隔是2+10=12s。
Q:
分場景看吧,我們先把定時執行的過程簡稱為周期性事件。
- 如果周期事件沒有執行完,就不執行下一個周期事件,那么上面的方法是推薦的;耗時如果比較長,比如 redis 的 serverCron 函數,就把持久化操作放在子進程去完成;
- 如果需要准確的定時,可以退化為 timer 里定義 timer 的方法,用多個 Thread 去執行;定時比較短的情況,此方法不推薦,會產生很多等待的 Thread,創建和回收都很耗資源,此時可以維護一個線程池避免這個問題