Python中线程的超时控制以及一个简单的应用
解决方案
一个线程不能优雅地杀死另一个线程,因此对于您当前的代码,它foo
永远不会终止。(使用thread.daemon = True
Python程序时,仅剩下守护程序线程将退出,但这不允许您在foo
不终止主线程的情况下终止。)
有些人试图使用信号来停止执行,但是在某些情况下这可能是不安全的。
如果可以修改foo
,则有许多解决方案。例如,您可以检查是否threading.Event
要退出while循环。
但是,如果您不能修改foo
,则可以使用该multiprocessing
模块在子进程中运行它,因为与线程不同,可以终止子进程。这是看起来的示例:
import time
import multiprocessing as mp
def foo(x = 1):
cnt = 1
while True:
time.sleep(1)
print(x, cnt)
cnt += 1
def timeout(func, args = (), kwds = {}, timeout = 1, default = None):
pool = mp.Pool(processes = 1)
result = pool.apply_async(func, args = args, kwds = kwds)
try:
val = result.get(timeout = timeout)
except mp.TimeoutError:
pool.terminate()
return default
else:
pool.close()
pool.join()
return val
if __name__ == '__main__':
print(timeout(foo, kwds = {'x': 'Hi'}, timeout = 3, default = 'Bye'))
print(timeout(foo, args = (2,), timeout = 2, default = 'Sayonara'))
产量
('Hi', 1)
('Hi', 2)
('Hi', 3)
Bye
(2, 1)
(2, 2)
Sayonara
注意,这也有一些限制。
-
子流程接收父流程变量的副本。如果您在子流程中修改变量,则不会影响父流程。如果您的函数
func
需要修改变量,则需要使用共享变量。 -
参数(通过传递
args
)和关键字(kwds
)必须是可腌制的。 - 进程比线程更多的资源。通常,您只想在程序开始时创建一次多处理池。每次调用时都会
timeout
创建此函数Pool
。这是必要的,因为我们需要pool.terminate()
终止foo
。
一、 简单介绍
线程的超时控制在实际的应用中肯定是广泛存在的,比如网络连接超时(socket),文件处理超时等等,但是现在的编程语言貌似都没有很好的处理机制来实现超时管理(也可能是我孤陋寡闻,知道的弟兄不妨赐教下,感激不尽!),一般的说法都是不要特意的去从外部杀死一个线程,退出线程的正确方法是让线程中的run()方法运行结束或者如果run()方法是一个循环在run()方法里面设置一个选项变量来控制循环终止条件(其实还是让run()“自然死亡”)。有些编程语言,比如Python,在其多线程机制里面,如 threading.Thread,根本没有提供终止线程的方法(http://docs.python.org/library/threading.html#thread-objects )。
那么我们怎么让线程超时退出呢,或者说怎么实现超时管理? 其实这需要一点策略。
在说这方面的事情之前,首先了解下怎么在python里面编写多线程的程序,让你的类继承 threading.Thread,并且在类的__init__()方法里面首先调用threading.Thread的__init__()方法,而且你的类必须有一个无参数的run()方法,比如下面的例子:
#!/usr/bin/python
# -*- coding: utf-8 -*-
import threading
########################################################################
class MyThread(threading.Thread):
"""A simple threading class."""
#----------------------------------------------------------------------
def __init__(self):
"""Constructor"""
threading.Thread.__init__(self, name = "Thread-1")
#----------------------------------------------------------------------
def run(self):
"""The working method, put all the work of the class in it."""
print "I am a threading class, my name is: %s " % self.getName()
print "I am stopping ..."
mythread = MyThread()
mythread.start()
二、Python中提供的线程超时检测机制
线程的超时与否可以用Python自己提供的机制来检测, 这就是线程的 join() 函数,在python的文档里面可以找到该函数的详细说明(http://docs.python.org/library/threading.html#threading.Thread.join)。 简单地说,如果同时执行了2个线程t1 和 t2,如果想让一个线程等待另一个线程执行结束再执行的话,那么必须执行被等待线程的join()方法,代码示例如下:
#----------------------------------------------------------------------
def test():
"""A task control function."""
... # previous job
t1 = Thread1()
t2 = Thread2()
t1.start()
t2.start()
t2.join(10) # wait here until t2 is over or timeout occured(10 seconds)
... # the next job
通过上面的链接查到join()方法的文档可以知道, 该方法有一个可选的参数 timeout, 如果像上面的例子中设置了该参数的话, 执行了该函数会在此等待t2线程10秒钟,在此期间调用程序(caller)什么也不做,就等着,直到t2结束了或者超时了,才会执行下面的代码。如果不设置timeout参数, caller会在此等待直到t2运行结束。这里需要注意的是, join()函数在这里只相当于一个“采样”,它不会在超时的时候终止t2的执行,实际上t2在超时的情况下还是会执行直到其结束或者另一种情况,caller结束了,但是前提是t2必须被设置为“守护线程(daemon)”(详情见下面的应用实例)。
--------------------------------------分割线 --------------------------------------
CentOS 6.4安装 Python2.7.10 http://www.linuxidc.com/Linux/2015-08/120895.htm
无需操作系统直接运行 Python 代码 http://www.linuxidc.com/Linux/2015-05/117357.htm
CentOS上源码安装Python3.4 http://www.linuxidc.com/Linux/2015-01/111870.htm
《Python核心编程 第二版》.(Wesley J. Chun ).[高清PDF中文版] http://www.linuxidc.com/Linux/2013-06/85425.htm
《Python开发技术详解》.( 周伟,宗杰).[高清PDF扫描版+随书视频+代码] http://www.linuxidc.com/Linux/2013-11/92693.htm
Python脚本获取Linux系统信息 http://www.linuxidc.com/Linux/2013-08/88531.htm
在Ubuntu下用Python搭建桌面算法交易研究环境 http://www.linuxidc.com/Linux/2013-11/92534.htm
Python 语言的发展简史 http://www.linuxidc.com/Linux/2014-09/107206.htm
--------------------------------------分割线 --------------------------------------
我们只是知道在那里等待了特定的时间再执行下面的代码,那么我们怎么判断t2是否是执行结束了还是线程超时了呢? 这就需要知道线程的"活动(alive)"的概念。大体上,一个线程自从start()方法被调用开始直到run()函数返回的这段期间都被认为是活动的, 而且python提供了一个方法 isAlive()来判断线程是否是活动的。对,就是这样,如果超时了的话,isAlive()方法肯定返回的True(因为join()方法不会结束线程,所以线程仍然是活动的), 而如果是执行结束了,run()函数肯定已经返回了,那么isAlive()方法肯定返回False。代码示例如下:
#----------------------------------------------------------------------
def test():
"""A task control function."""
... # previous job
t1 = Thread1()
t2 = Thread2()
t1.start()
t2.start()
t2.join(10) # wait here until t2 is over or timeout occured(10 seconds)
if t2.isAlive(): # if t2 is still alive, then it is time out!
print 't2 is time out!'
... # the next job
三、应用实例
超时以及python中的基本超时处理在上面已经为读者给出了,在这部分我将介绍一个超时控制和管理的简单实例。在这个例子中,我将有10个特种部队队员去执行刺杀恐怖分子的任务,每个人都有自己的刺杀目标,必须把自己的目标干掉才算完事! 每个人执行任务的时间肯定是不一样的,但是撤退的直升机只等待特定的时间就起飞,这意味着肯定有人因为超时而无法撤退,超时的人执行任务失败!
类1: Soldier, 执行任务的类。 有两个参数在这个类里面起到了至关重要的作用即 isStopped和isSuccess,前者说明这个类是否执行结束,后者说明这个类是否执行成功。 另外, isStopped还用于判断这个类是否是超时的, 我在这里不用前面说的isAlive()函数来判断,是因为isAlive()函数时间上太严格了,我需要让类自己设置一个是否停止的标志(我曾经看到过类已经运行完了,但是isAlive()函数还是返回True的情况)。每一个Soldier类都调用setDaemon(True)方法被设置为守护线程(daemon),所谓守护线程,就是在caller执行完毕的情况下,该线程也会结束,否则该线程会继续执行直到其真正的执行结束。Soldier类如下:
#!/usr/bin/python
# -*- coding: utf-8 -*-
import os, sys, re, math
import threading
import time
import random
########################################################################
class Soldier(threading.Thread):
"""The class of a soldier."""
#----------------------------------------------------------------------
def __init__(self, name):
"""Constructor"""
threading.Thread.__init__(self, name = name)
self.name = name # the name of this soldier
self.setDaemon(True) # this is a daemon thread.
# the time of this soldier need to finish the job
self.playTime = random.randint(1,10)
# is the soldier stop shotting, if timeout or the target has been killed,
# he may stop.
self.isStopped = False
self.isSuccess = False # did he kill the target?
#----------------------------------------------------------------------
def assassinate(self):
"""The task, to kill the target."""
for i in range(self.playTime):
print '%s play(%d)' % (self.name, i+1)
time.sleep(1)
#----------------------------------------------------------------------
def run(self):
"""Start to move ..."""
print '%s has moved out, need time: %d ...' % (self.name, self.playTime)
self.assassinate()
print '%s stopped ...' % self.name
self.isStopped = True # the target has been killed, then he stopped.
self.isSuccess = True
类2: Commander, 指挥Soldier的类。这个类不是为了增加戏剧效果而特意加上的,而是这个类是必须的。执行任务的类需要有一个中间调用者来为其调用join()方法判断其是否超时。没错caller也可以在其开始执行的时候依次调用工作者类的join()方法来判断超时,但是这样的话,每次调用join()都需要等待,相当于线程不是一起执行而是一个接一个地执行。因此,为每个工作者类设计一个中间的调用类是必须的,caller依次启动这些中间的调用类,使这些类一起运行,那么所有的工作者类就是一起工作的,这才是多线程。 Commander类如下:
########################################################################
class Commander(threading.Thread):
"""The class of commander, a commander will command only one soldier."""
#----------------------------------------------------------------------
def __init__(self, soldier):
"""Constructor"""
threading.Thread.__init__(self, name = 'Commander')
self.soldier = soldier
#----------------------------------------------------------------------
def run(self):
"""Authorize the soldier to start killing."""
self.soldier.start()
try:
# Boss said: give the soldier 5 seconds to finish his job
self.soldier.join(5)
except:
pass
# Use the class's own attribute to judge whether it is timeout.
#if self.soldier.isAlive():
if not self.soldier.isStopped:
print '%s is timeout!' % self.soldier.name
# the soldier run out his time, then he stopped.
self.soldier.isStopped = True
Caller: 执行所有的工作,包括初始化工作者类和中间调用类,到了规定时间检查执行结果等, caller的代码如下:
#----------------------------------------------------------------------
def killing():
"""Let's pull the trigger, start killing !"""
t1 = time.time()
# Get ready for the commanders
l_commander = []
for i in range(10): # 10 soldiers
# get ready for the soldier
soldier = Soldier('soldier-%d' % (i+1))
if i == 5 or i == 9:
soldier.playTime = 10000
l_commander.append(Commander(soldier))
# Soldiers move out one by one.
for cmd in l_commander:
cmd.start()
# Judge whether the helicopter should go. If all the soldiers are stop,
# that is, all finished job or timeout, then it should go!
isBreak = False
while not isBreak:
isBreak = True
for cmd in l_commander:
if cmd.soldier.isStopped == False:
isBreak = False
# Check the results of the battle at the schedule time.
for cmd in l_commander:
print '%s, is success: %s' % (cmd.soldier.name, cmd.soldier.isSuccess)
# Go back to base.
time.sleep(20)
# Check the results at the final time.
for cmd in l_commander:
print '%s, is success: %s' % (cmd.soldier.name, cmd.soldier.isSuccess)
t2 = time.time()
print 'Total time: %.2f' % (float(t2-t1))
在caller中,用一个while循环来控制等待所有的线程stop而不执行下面的代码,一旦所有的线程stop了,则检查执行结果。
执行 killing()函数的结果如下:
soldier-1 has moved out, need time: 2 ...
soldier-1 play(1)
soldier-2 has moved out, need time: 6 ...
soldier-2 play(1)
soldier-3 has moved out, need time: 3 ...
soldier-3 play(1)
soldier-4 has moved out, need time: 4 ...
soldier-4 play(1)
soldier-5 has moved out, need time: 9 ...
soldier-5 play(1)
soldier-6 has moved out, need time: 10000 ...
soldier-6 play(1)
soldier-7 has moved out, need time: 8 ...
soldier-7 play(1)
soldier-8 has moved out, need time: 10 ...
soldier-8 play(1)
soldier-9 has moved out, need time: 7 ...
soldier-9 play(1)
soldier-10 has moved out, need time: 10000 ...
soldier-10 play(1)
soldier-3 play(2)
soldier-2 play(2)
soldier-4 play(2)
soldier-1 play(2)
soldier-6 play(2)
soldier-7 play(2)
soldier-5 play(2)
soldier-8 play(2)
soldier-9 play(2)
soldier-10 play(2)
soldier-1 stopped ...
soldier-3 play(3)
soldier-2 play(3)
soldier-7 play(3)
soldier-6 play(3)
soldier-5 play(3)
soldier-4 play(3)
soldier-8 play(3)
soldier-9 play(3)
soldier-10 play(3)
soldier-7 play(4)
soldier-6 play(4)
soldier-3 stopped ...
soldier-4 play(4)
soldier-2 play(4)
soldier-8 play(4)
soldier-5 play(4)
soldier-9 play(4)
soldier-10 play(4)
soldier-7 play(5)
soldier-6 play(5)
soldier-4 stopped ...
soldier-2 play(5)
soldier-8 play(5)
soldier-5 play(5)
soldier-9 play(5)
soldier-10 play(5)
soldier-6 is timeout!
soldier-2 is timeout!
soldier-7 is timeout!
soldier-8 is timeout!
soldier-5 is timeout!
soldier-7 play(6)
soldier-6 play(6)
soldier-2 play(6)
soldier-8 play(6)
soldier-5 play(6)
soldier-9 is timeout!
soldier-9 play(6)
soldier-10 is timeout!
soldier-1, is success: True
soldier-2, is success: False
soldier-3, is success: True
soldier-4, is success: True
soldier-5, is success: False
soldier-6, is success: False
soldier-7, is success: False
soldier-8, is success: False
soldier-9, is success: False
soldier-10, is success: False
soldier-10 play(6)
soldier-7 play(7)
soldier-6 play(7)
soldier-2 stopped ...
soldier-8 play(7)
soldier-5 play(7)
soldier-9 play(7)
soldier-10 play(7)
soldier-7 play(8)
soldier-6 play(8)
soldier-8 play(8)
soldier-5 play(8)
soldier-9 stopped ...
soldier-10 play(8)
soldier-7 stopped ...
soldier-6 play(9)
soldier-8 play(9)
soldier-5 play(9)
soldier-10 play(9)
soldier-6 play(10)
soldier-5 stopped ...
soldier-8 play(10)
soldier-10 play(10)
soldier-6 play(11)
soldier-8 stopped ...
soldier-10 play(11)
soldier-6 play(12)
soldier-10 play(12)
soldier-6 play(13)
soldier-10 play(13)
soldier-6 play(14)
soldier-10 play(14)
soldier-6 play(15)
soldier-10 play(15)
soldier-6 play(16)
soldier-10 play(16)
soldier-6 play(17)
soldier-10 play(17)
soldier-6 play(18)
soldier-10 play(18)
soldier-6 play(19)
soldier-10 play(19)
soldier-6 play(20)
soldier-10 play(20)
soldier-6 play(21)
soldier-10 play(21)
soldier-6 play(22)
soldier-10 play(22)
soldier-6 play(23)
soldier-10 play(23)
soldier-6 play(24)
soldier-10 play(24)
soldier-6 play(25)
soldier-10 play(25)
soldier-6 play(26)
soldier-1, is success: True
soldier-2, is success: True
soldier-3, is success: True
soldier-4, is success: True
soldier-5, is success: True
soldier-6, is success: False
soldier-7, is success: True
soldier-8, is success: True
soldier-9, is success: True
soldier-10, is success: False
Total time: 25.05
结果中显示在5秒钟的时候检查战果, 只有1,3和4圆满的完成任务, 其他都超时。然后,在回到基���的20秒中里面, 其他的未完成的线程并没有停止,而是继续在工作, 20秒之后, caller也结束了,注意所有的工作者线程都已经设置成了守护线程,所以在caller结束的时候,也都跟着结束了。 在caller中,为了说明执行任务的类设置为守护线程与非守护线程的区别, 我特地让6号和10号队员的时间增加, 所以在最后将要结束的时候检查战果, 6和10还是没有完成, 依旧超时。
如果将Soldier类中的 self.setDaemon(True) 注释掉,那么6和10将会在caller结束的时候继续执行,直到10000秒后其真的运行结束。
四、总结
线程的超时控制和处理是非常实用的技术, 平时会用到很多。利用python自身提供的检测方法和自定义的控制项,我们可以很好地实现超时控制和管理。 在本文第三部分的例子中, 我展示了一种超时控制的策略, 即 工作者类+调用类+caller的模型, 里面的每个属性都是此模型里面的基础的和必须的。这个模型我目前正应用在了本人的一个项目上面, 在这个项目里面, 需要使用许多的计算机方法来处理一个蛋白质,每个方法都相当于本文例子里面的Soldier类,一个caller需要得到每个方法的处理结果,然后进行综合分析。 为了不让整个系统的执行时间被某个方法拖的很长,必须对每个处理蛋白质的方法进行超时控制, 在规定的时间里面如果该方法没有给出结果,自动将其忽略。