如何限制Python中函数调用的执行时间


73

我的代码中有一个与套接字相关的函数调用,该函数来自另一个模块,因此不受我的控制,问题是它有时阻塞数小时,这是完全不可接受的,如何限制我代码中函数的执行时间?我猜该解决方案必须利用另一个线程。


Answers:


40

我不确定这可能是跨平台的,但是使用信号和警报可能是查看此情况的好方法。只需做一些工作,您就可以使它在任何情况下都完全通用。

http://docs.python.org/library/signal.html

因此,您的代码将看起来像这样。

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!"

如果我在其他地方使用警报器怎么办?:-)

2
是的,可能值得选择其中一个。我本人是一个流程/信号员。看了blip.tv/file/2232410之后,我发现自己越来越不相信Python的线程模型了。
rik.the.vik

10
另外,这不会在以后禁用警报
Casebash


6
signal.alarm和signal.SIGALRM仅在Unix上可用。
洛根

102

@ rik.the.vik答案的一个改进是使用该with语句为超时功能提供一些语法功能:

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!")

2
try: yield \n finally: signal.alarm(0)
jfs

要大惊小怪,signal.alarm(seconds)是否属于try?
Casebash

好吧,它没有记录为引发任何异常,因此可能没有。
乔什·李

3
而不是最后一个“超时!” 您可能打算写msg
Olga

你好!我试图在Python 3.4中实现此方法,并遇到一个错误,我很难解决:AttributeError:exit 追溯仅提供以下内容:File“ <stdin>”,<module>中的第1行解决这个问题?
David Scott

20

这是一种限制功能运行时间的Linux / OSX方法。这是在您不想使用线程,并且希望您的程序等到函数结束或时间限制到期的情况下。

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

5
在我的Linux笔记本电脑上完美运行,在Windows上不运行,在OSX上几乎不运行。不是您的错,只是编程的美好世界。
Ameo,2016年

1
@CaseyPrimozic您正在生成新进程,这可能是原因。我会更正Unix,而不是跨平台的答案
Ariel Cabib

18

我更喜欢上下文管理器方法,因为它允许在一条with time_limit语句中执行多个python语句。由于Windows系统没有此功能SIGALARM,因此可以使用一种更便携,更直接的方法Timer

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)

这里的关键技术是使用_thread.interrupt_main中断定时器线程的主线程。一个需要注意的是,主线程并不总是以响应KeyboardInterrupt由凸起Timer迅速。例如,time.sleep()调用系统功能,因此KeyboardInterrupt将在sleep调用后处理a 。


1
这看起来最干净。有什么方法可以将中断与用户发送的键盘中断区分开来吗?

顺便说一句,这将杀死所有线程,而不适合用作杀死多线程操作中的空闲线程的计时器
Alexander McFarlane

由于thread模块已_thread在Python 3中重命名为与Python 2兼容,因此您需要执行以下操作:import sys if sys.version_info[0] < 3: from thread import interrupt_main else: from _thread import interrupt_main然后再执行以下操作:timer = threading.Timer(seconds, lambda: interrupt_main())
Roland Pihlakas,

7

从信号处理程序内部执行此操作很危险:在引发异常时,您可能位于异常处理程序内部,并使事物处于中断状态。例如,

def function_with_enforced_timeout():
  f = open_temporary_file()
  try:
   ...
  finally:
   here()
   unlink(f.filename)

如果在here()引发异常,则永远不会删除该临时文件。

此处的解决方案是将异步异常推迟到该代码不在异常处理代码(“ except”或“ finally”块)之内,但是Python不会这样做。

注意,这不会在执行本机代码时中断任何内容。它只会在函数返回时中断它,因此这可能对这种特殊情况没有帮助。(SIGALRM本身可能会中断正在阻塞的调用,但是套接字代码通常只在EINTR之后重试。)

用线程执行此操作是一个更好的主意,因为它比信号更可移植。由于您是在启动工作线程并阻塞直到完成,因此没有通常的并发后顾之忧。不幸的是,无法将异常异步传递给Python中的另一个线程(其他线程API可以做到这一点)。在异常处理程序中发送异常也会遇到相同的问题,并且需要相同的修复程序。


PyErr_CheckSignals()在EINTR上重新启动之前,本机代码可能会调用,以允许Python信号处理程序运行。
jfs

5

您不必使用线程。您可以使用另一个进程来完成阻塞工作,例如,可以使用进程模块。如果您想在程序的不同部分之间共享数据结构,那么Twisted是一个不错的库,它可以让您自己控制它,如果您担心阻塞并希望遇到很多麻烦,我建议您这样做。Twisted的坏消息是您必须重写代码以避免任何阻塞,而且学习过程还很公平。

可以使用线程来避免阻塞,但是我认为这是万不得已的做法,因为它使您陷入痛苦的整个世界。在甚至考虑在生产中使用线程之前,请阅读一本关于并发的好书,例如Jean Bacon的“ Concurrent Systems”。我与一群真正使用线程来冷却高性能的人一起工作,除非真正需要线程,否则我们不会将线程引入项目中。


4

不管是哪种语言,唯一的“安全”方法是使用辅助进程来执行该超时操作,否则,您需要以一种可以自行安全地超时的方式构建代码,例如通过检查循环或类似情况下经过的时间。如果不能选择更改方法,则线程不足。

为什么?因为这样做时您有冒险使事情保持糟糕状态的危险。如果只是简单地中途终止了线程,则持有的锁等只会被持有,而无法释放。

所以看的过程中顺便说一下,看线程的方式。


1

我通常会喜欢使用@ josh-lee建议的contextmanager

但是,如果有人有兴趣将其实现为装饰器,则可以选择另一种方法。

它是这样的:

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')

这是timeout.py模块:

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

输出:

python
rocks
A Done
timing
Traceback (most recent call last):
  ...
timeout.TimeoutError: execution expired
out

请注意,即使TimeoutError抛出,修饰的方法也将继续在其他线程中运行。如果您还希望该线程“停止”,请参见:有什么方法可以杀死Python中的线程?


1-没有中断方法的InterruptableThread类。2-即使执行终止后,线程仍在执行该功能
Sergey11g,2010年

0

我认为这是我通过Google找到的超时功能,它对我有用。

来自:http : //code.activestate.com/recipes/473878/

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   

2
为什么if / else返回块的两个部分都返回it.result?
NorthIsUp

那是我针对正在处理的特定问题的编辑。我不记得为什么。无论如何,如果它是原版,则返回“默认”。
monkut

请注意,此功能不会在超时后停止线程,而留下未完成的僵尸线程使用资源。
Marcos Vives Del Sol

0

@ user2283347中的方法已经过测试,可以工作,但是我们希望摆脱追溯消息。在Ctrl-C上使用Python中的Remove traceback中的pass技巧,修改后的代码为:

from contextlib import contextmanager
import threading
import _thread

class TimeoutException(Exception): pass

@contextmanager
def time_limit(seconds):
    timer = threading.Timer(seconds, lambda: _thread.interrupt_main())
    timer.start()
    try:
        yield
    except KeyboardInterrupt:
        pass     
    finally:
        # if the action ends in specified time, timer is canceled
        timer.cancel()

def timeout_svm_score(i):
     #from sklearn import svm
     #import numpy as np
     #from IPython.core.display import display
     #%store -r names X Y
     clf = svm.SVC(kernel='linear', C=1).fit(np.nan_to_num(X[[names[i]]]), Y)
     score = clf.score(np.nan_to_num(X[[names[i]]]),Y)
     #scoressvm.append((score, names[i]))
     display((score, names[i])) 
     
%%time
with time_limit(5):
    i=0
    timeout_svm_score(i)
#Wall time: 14.2 s

%%time
with time_limit(20):
    i=0
    timeout_svm_score(i)
#(0.04541284403669725, '计划飞行时间')
#Wall time: 16.1 s

%%time
with time_limit(5):
    i=14
    timeout_svm_score(i)
#Wall time: 5h 43min 41s

我们可以看到,此方法可能需要很长的时间来中断计算,我们要求5秒钟,但要花5个小时才能解决。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.