昨天为我的 casnet 程序添加新功能。其中一个功能是断线自动重连,本来是单线程的程序,添加这个功能就需要后台有一个线程定时地查询当前状态,如果掉线就自动重连。因之遇到了一个如何设计这个守护线程的问题。
我刚开始的想法是后台线程每次运行查询后 sleep 一段时间,然后再运行查询。但是我马上遇到了一个问题:当主程序退出时,后台线程仍在运行,主窗口无法退出。
在使用其它的库时,比如 POSIX 的 pthread,可以使用 ptread_cancel(tid) 在主线程中结束子线程。但是 Python 的线程库不支持这样做,理由是我们不应该强制地结束一个线程,这样会带来很多隐患,应该让该线程自己结束自己。所以在 Python 中,推荐的一种方法是在子线程中循环判断一个标志位,在主线程中改变该标志位,子线程读到标志位改变,就结束自己。import threading
class X(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.flag = 1
def run(self):
while self.flag == 1:
sleep(300)
...
如果直接使用这种方法,那么我前面的设计就会出现问题。因为线程会被 sleep 阻塞一段时间,那么只有在 sleep 的间隙,才有可能去读取标志位。这样主线程需要等待当前 sleep 结束才能使子线程退出,进而整个程序才能退出。这种做法是行不通的,你不可能指望用户点击“关闭窗口”后等待几百秒程序才能退出。
当然,也可以使用系统命令 kill 来杀死整个进程。但问题是这样做既不 graceful,又不能保证代码对不同系统的兼容性。
只好换个思路,从原来后台进程的设计改起。定时执行未必非得使用 sleep,也可以像 crontab 那样判断当前时间能不能整除某个值,但这样做不能保证任务在某个时间间隔内只执行一次,因为除数的精度和任务的执行时间不好把握;或者使用 timer,但是 timer 会带来更多线程,增加了复杂度。
于是最后决定使用解决 Feedbuner 图标定时抓取问题的方法。在线程中保存上次查询时间,比较当前时间与上次查询时间的差,若大于某个值,就进行查询并更新保存的时间。def run(self):
self.last = time.time()
while self.flag == 1:
Now = time.time()
if Now - self.last > 300:
self.last = Now
...
这样就既能保证子线程在 flag 改变之后尽快退出,又能保证在指定时间间隔内任务只运行一次。但是网友 earthengine 兄指出这种方法并不妥,代码中不用 sleep 就变成了忙循环,这样会造成 CPU 使用率过高的问题,仅仅在循环中间添加一个 sleep(0~1) 就能大幅度地降低 CPU 使用,而且关闭程序时 1 秒钟以内的延迟对于用户来说一般还是可以接受的。def run(self):
self.last = time.time()
while self.flag == 1:
sleep(1)
Now = time.time()
if Now - self.last > 300:
self.last = Now
...
再深入思考一下,虽然本文中的后台线程从功能上来看似乎用不着考虑太多同步的问题,但最后的退出过程可视为一个线程同步的过程。因此可以采用线程同步的思想来设计后台线程:在正常工作时,后台线程进行带超时的等待,超时后就执行工作;退出时主线程给后台线程发送一个信号,由于后台线程在超时等待,因此接收信号后就终止退出。这样,在用户结束程序时,就不用等待 sleep 到时了。import threading
class X(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.flag = 1
self.cond = threading.Condition()
def run(self):
self.cond.acquire()
self.condition.wait(300)
while self.flag == 1:
...
self.cond.release()
self.cond.acquire()
self.condition.wait(300)
...
x.flag = 0
x.cond.acquire()
x.cond.notify()
x.cond.release()
最后,非常感谢 earthengine 兄的精彩评论,小弟受益良多。