How to limit execution time of a function call in Python
在我的代码中有一个与套接字相关的函数调用,该函数来自另一个模块,因此不受我的控制,问题是它偶尔会阻塞几个小时,这是完全不可接受的,我如何限制代码中的函数执行时间? 我想解决方案必须使用另一个线程。
@ rik.the.vik答案的一个改进是使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | import signal from contextlib import contextmanager class TimeoutException(Exception): pass @contextmanager def time_limit(seconds): def signal_handler(signum, frame): raise TimeoutException("Timed out!") signal.signal(signal.SIGALRM, signal_handler) signal.alarm(seconds) try: yield finally: signal.alarm(0) try: with time_limit(10): long_function_call() except TimeoutException as e: print("Timed out!") |
我不确定这可能是跨平台的,但使用信号和警报可能是一个很好的方式来看待这个。通过一些工作,您可以使其完全通用,并且可以在任何情况下使用。
http://docs.python.org/library/signal.html
所以你的代码看起来像这样。
1 2 3 4 5 6 7 8 9 10 11 | import signal def signal_handler(signum, frame): raise Exception("Timed out!") signal.signal(signal.SIGALRM, signal_handler) signal.alarm(10) # Ten seconds try: long_function_call() except Exception, msg: print"Timed out!" |
这是限制函数运行时间的Linux / OSX方法。这是因为您不想使用线程,并希望您的程序等到函数结束或时间限制到期。
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 | from multiprocessing import Process from time import sleep def f(time): sleep(time) def run_with_limited_time(func, args, kwargs, time): """Runs a function with time limit :param func: The function to run :param args: The functions args, given as tuple :param kwargs: The functions keywords, given as dict :param time: The time limit in seconds :return: True if the function ended successfully. False if it was terminated. """ p = Process(target=func, args=args, kwargs=kwargs) p.start() p.join(time) if p.is_alive(): p.terminate() return False return True if __name__ == '__main__': print run_with_limited_time(f, (1.5, ), {}, 2.5) # True print run_with_limited_time(f, (3.5, ), {}, 2.5) # False |
我更喜欢上下文管理器方法,因为它允许在
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 | from contextlib import contextmanager import threading import _thread class TimeoutException(Exception): def __init__(self, msg=''): self.msg = msg @contextmanager def time_limit(seconds, msg=''): timer = threading.Timer(seconds, lambda: _thread.interrupt_main()) timer.start() try: yield except KeyboardInterrupt: raise TimeoutException("Timed out for operation {}".format(msg)) finally: # if the action ends in specified time, timer is canceled timer.cancel() import time # ends after 5 seconds with time_limit(5, 'sleep'): for i in range(10): time.sleep(1) # this will actually end after 10 seconds with time_limit(5, 'sleep'): time.sleep(10) |
这里的关键技术是使用
在信号处理程序中执行此操作很危险:在引发异常时您可能位于异常处理程序中,并使事物处于损坏状态。例如,
1 2 3 4 5 6 7 | def function_with_enforced_timeout(): f = open_temporary_file() try: ... finally: here() unlink(f.filename) |
如果在此处引发异常(),则永远不会删除临时文件。
这里的解决方案是推迟异步异常,直到代码不在异常处理代码(除外或最后一个块)内,但Python不这样做。
请注意,执行本机代码时不会中断任何操作;它只会在函数返回时中断它,所以这可能对这种特殊情况没有帮助。 (SIGALRM本身可能会中断阻塞的调用 - 但是套接字代码通常只是在EINTR之后重试。)
使用线程执行此操作是一个更好的主意,因为它比信号更便携。因为你正在启动一个工作线程并阻塞直到它完成,所以没有通常的并发担忧。不幸的是,没有办法异步地将异常传递给Python中的另一个线程(其他线程API可以做到这一点)。在异常处理程序中发送异常也会遇到同样的问题,并且需要相同的修复。
您不必使用线程。您可以使用其他进程来执行阻止工作,例如,可能使用子进程模块。如果你想在你的程序的不同部分之间共享数据结构,那么Twisted是一个很好的库,可以让你自己控制它,如果你关心阻塞并且期望遇到很多麻烦,我会推荐它。 Twisted的坏消息是你必须重写你的代码以避免任何阻塞,并且有一个公平的学习曲线。
你可以使用线程来避免阻塞,但我认为这是最后的手段,因为它会让你暴露在一个痛苦的世界里。在考虑在生产中使用线程之前,请阅读一本关于并发性的好书,例如:让·培根的"并行系统"。我与一群用线程真正冷却高性能东西的人合作,除非我们确实需要,否则我们不会将线程引入项目。
在任何语言中,唯一的"安全"方法是使用辅助进程来执行超时操作,否则您需要以这样一种方式构建代码,使其能够安全地自行超时,例如检查循环或类似过程中经过的时间。如果不能选择更改方法,则线程将不够。
为什么?因为你冒着把事情弄得很糟糕的风险。如果线程在方法中被简单地杀死,则保持锁定等将被保持,并且不能被释放。
所以看一下进程的方式,不要看线程的方式。
我通常更喜欢使用@josh-lee建议的上下文管理器
但是如果有人有兴趣将其作为装饰器实现,这里有一个替代方案。
这是它的样子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | import time from timeout import timeout class Test(object): @timeout(2) def test_a(self, foo, bar): print foo time.sleep(1) print bar return 'A Done' @timeout(2) def test_b(self, foo, bar): print foo time.sleep(3) print bar return 'B Done' t = Test() print t.test_a('python', 'rocks') print t.test_b('timing', 'out') |
这是
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 | import threading class TimeoutError(Exception): pass class InterruptableThread(threading.Thread): def __init__(self, func, *args, **kwargs): threading.Thread.__init__(self) self._func = func self._args = args self._kwargs = kwargs self._result = None def run(self): self._result = self._func(*self._args, **self._kwargs) @property def result(self): return self._result class timeout(object): def __init__(self, sec): self._sec = sec def __call__(self, f): def wrapped_f(*args, **kwargs): it = InterruptableThread(f, *args, **kwargs) it.start() it.join(self._sec) if not it.is_alive(): return it.result raise TimeoutError('execution expired') return wrapped_f |
输出:
1 2 3 4 5 6 7 8 | python rocks A Done timing Traceback (most recent call last): ... timeout.TimeoutError: execution expired out |
请注意,即使抛出
这是我认为通过谷歌找到的超时功能,它适用于我。
从:
http://code.activestate.com/recipes/473878/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | def timeout(func, args=(), kwargs={}, timeout_duration=1, default=None): '''This function will spwan a thread and run the given function using the args, kwargs and return the given default value if the timeout_duration is exceeded ''' import threading class InterruptableThread(threading.Thread): def __init__(self): threading.Thread.__init__(self) self.result = default def run(self): try: self.result = func(*args, **kwargs) except: self.result = default it = InterruptableThread() it.start() it.join(timeout_duration) if it.isAlive(): return it.result else: return it.result |