昨天为我的 casnet 程序添加新功能。其中一个功能是断线自动重连,本来是单线程的程序,添加这个功能就需要后台有一个线程定时地查询当前状态,如果掉线就自动重连。因之遇到了一个如何设计这个守护线程的问题。
我刚开始的想法是后台线程每次运行查询后 sleep 一段时间,然后再运行查询。但是我马上遇到了一个问题:当主程序退出时,后台线程仍在运行,主窗口无法退出。
在使用其它的库时,比如 POSIX 的 pthread,可以使用 ptread_cancel(tid) 在主线程中结束子线程。但是 Python 的线程库不支持这样做,理由是我们不应该强制地结束一个线程,这样会带来很多隐患,应该让该线程自己结束自己。所以在 Python 中,推荐的一种方法是在子线程中循环判断一个标志位,在主线程中改变该标志位,子线程读到标志位改变,就结束自己。
import threading
class X(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.flag = 1def 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 兄的精彩评论,小弟受益良多。
不错的分析,挺好。
以前折腾过Pxpython, 也遇到类似的问题
这样的代码很不好,不用Sleep就成了“忙”循环,会耗用大量CPU时间去做无用的事情。哪怕加一个Sleep(0)也好很多。
如果我来设计,我会推荐使用定时器结合同步信号的方法。工作线程正常时阻塞于一个同步信号灯,定时器每次激活的时候向正在等待的工作发送这个信号把工作线程激活。工作线程激活后首先检查退出标志,如果可以退出就退出好了。否则就干一次活,然后释放信号灯继续等下次别的线程把它激活。主线程要退出的时候,首先把退出标志设置好,然后就激活工作线程,就peacefuly结束了它的运行。
其实多引入一个像上面描述的那么简单的线程没什么大不了,如果把相关的代码全部封装在一个类里面,一两页就写完了,简单的逻辑验证起来也不会费劲。
其实,还有更简单的方法,不需要定时器。我不会Python,不知道它的同步机制,但多数语言的同步机制都包括带超时的等待。如果Python支持,那么你的工作线程可以不断在同步信号灯上进行超时等待。如果等到了信号,因为取消了定时器,那一定是主线程来的,所以就可以退出了。如果超时了,那就该干活了。干完之后接着等下一个信号。超时时间的设置可以沿用你那个Now-self.last的算法,就不需要定时器也能实现需求了。
python 有 SemaphoreObjects
随便一个能实现 wait和wake_up的东西都能拿来用
http://www.python.org/doc/2.5.2/lib/condition-objects.html
参考了一下Python的线程库,最合用的同步对象应该是条件变量。但是它的wait方法虽然带有超时,却无法分辨是超时返回还是被激活。所以还是需要一个标志来告知工作线程这个信息。
原本以为这样已经是最简单解法了,谁知道还有更简单的。
http://www.python.org/doc/2.5.2/lib/thread-objects.html
直接在线程对象上使用join()方法,即可让一个线程等待另一个线程退出。此方法支持超时。在超时后对目标线程使用isAlive()方法,即可知道那个线程是否已经退出运行。结合这两个方法,可以把附加的同步变量和标志全部取消而实现需求(算法其实没有变化,在同步变量上的等待改为join,对标志的检查改为isAlive即可)
python 2.6 里,用 subprocess 生成子进程,可以被杀死
Popen.kill()¶
Kills the child. On Posix OSs the function sends SIGKILL to the child. On Windows kill() is an alias for terminate().
New in version 2.6.
求教,如果线程在干一次活的时候卡死来?该怎么杀掉该线程?