簡體   English   中英

如何限制函數調用的執行時間?

[英]How to limit execution time of a function call?

我的代碼中有一個與套接字相關的函數調用,該函數來自另一個模塊,因此不受我的控制,問題是它偶爾會阻塞幾個小時,這是完全不可接受的,如何從我的代碼中限制函數執行時間? 我想解決方案必須使用另一個線程。

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

我不確定這可能是多么跨平台,但使用信號和警報可能是看待這個問題的好方法。 通過一些工作,您可以使其完全通用並且在任何情況下都可以使用。

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

這是限制函數運行時間的 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

我更喜歡上下文管理器方法,因為它允許在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從定時器線程中斷主線程。 一個警告是主線程並不總是快速響應Timer引發的KeyboardInterrupt 例如, time.sleep()調用一個系統函數,因此在sleep調用之后將處理KeyboardInterrupt

這里:獲得所需效果的簡單方法:

https://pypi.org/project/func-timeout

這救了我的命。

現在舉一個關於它如何工作的例子:假設你有一個巨大的項目列表要處理,並且你正在對這些項目迭代你的函數。 但是,由於某些奇怪的原因,您的函數卡在第 n 項上,而沒有引發異常。 您需要對其他項目進行處理,越多越好。 在這種情況下,您可以設置處理每個項目的超時時間:

import time
import func_timeout


def my_function(n):
    """Sleep for n seconds and return n squared."""
    print(f'Processing {n}')
    time.sleep(n)
    return n**2


def main_controller(max_wait_time, all_data):
    """
    Feed my_function with a list of itens to process (all_data).

    However, if max_wait_time is exceeded, return the item and a fail info.
    """
    res = []
    for data in all_data:
        try:
            my_square = func_timeout.func_timeout(
                max_wait_time, my_function, args=[data]
                )
            res.append((my_square, 'processed'))
        except func_timeout.FunctionTimedOut:
            print('error')
            res.append((data, 'fail'))
            continue

    return res


timeout_time = 2.1  # my time limit
all_data = range(1, 10)  # the data to be processed

res = main_controller(timeout_time, all_data)
print(res)

從信號處理程序中執行此操作是危險的:您可能在引發異常時位於異常處理程序中,並使事情處於損壞狀態。 例如,

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

如果在此處()引發您的異常,則永遠不會刪除臨時文件。

這里的解決方案是將異步異常推遲到代碼不在異常處理代碼中(except 或 finally 塊),但 Python 不這樣做。

請注意,這不會在執行本機代碼時中斷任何內容; 它只會在函數返回時中斷它,所以這可能無助於這種特殊情況。 (SIGALRM 本身可能會中斷阻塞的調用——但套接字代碼通常只是在 EINTR 之后重試。)

使用線程執行此操作是一個更好的主意,因為它比信號更便攜。 由於您正在啟動一個工作線程並阻塞直到它完成,所以沒有通常的並發問題。 不幸的是,沒有辦法將異常異步傳遞給 Python 中的另一個線程(其他線程 API 可以做到這一點)。 在異常處理程序期間發送異常也會有同樣的問題,並且需要相同的修復。

您不必使用線程。 您可以使用另一個進程來完成阻塞工作,例如,可能使用subprocess模塊。 如果您想在程序的不同部分之間共享數據結構,那么Twisted是一個很好的庫,可以讓您自己控制它,如果您關心阻塞並希望遇到很多麻煩,我會推薦它。 Twisted 的壞消息是你必須重寫你的代碼以避免任何阻塞,並且有一個公平的學習曲線。

可以使用線程來避免阻塞,但我認為這是最后的手段,因為它會讓您面臨整個世界的痛苦。 在考慮在生產中使用線程之前閱讀一本關於並發的好書,例如 Jean Bacon 的“並發系統”。 我和一群用線程做非常酷的高性能工作的人一起工作,除非我們真的需要線程,否則我們不會將線程引入項目中。

在任何語言中,執行此操作的唯一“安全”方法是使用輔助進程來執行該超時操作,否則您需要以這樣一種方式構建代碼,使其自身安全超時,例如通過檢查循環中經過的時間或類似的時間。 如果更改方法不是一種選擇,那么線程將不夠用。

為什么? 因為當你這樣做時,你會冒着讓事情處於糟糕狀態的風險。 如果線程在方法中被簡單地殺死,被持有的鎖等將被持有,並且不能被釋放。

所以看進程方式,不要看線程方式。

我通常更喜歡使用@josh-lee 建議的上下文管理器

但如果有人有興趣將其作為裝飾器實現,這里有一個替代方案。

這是它的樣子:

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被拋出,被裝飾的方法將繼續在不同的線程中運行。 如果您還希望該線程被“停止”,請參閱: Is there any way to kill a Thread in Python?

使用簡單的裝飾器

這是我在研究上述答案后制作的版本。 很直接。

def function_timeout(seconds: int):
    """Wrapper of Decorator to pass arguments"""

    def decorator(func):
        @contextmanager
        def time_limit(seconds_):
            def signal_handler(signum, frame):  # noqa
                raise TimeoutException(f"Timed out in {seconds_} seconds!")

            signal.signal(signal.SIGALRM, signal_handler)
            signal.alarm(seconds_)
            try:
                yield
            finally:
                signal.alarm(0)

        @wraps(func)
        def wrapper(*args, **kwargs):
            with time_limit(seconds):
                return func(*args, **kwargs)

        return wrapper

    return decorator

如何使用?

@function_timeout(seconds=5)
def my_naughty_function():
    while True:
        print("Try to stop me ;-p")

當然,如果該函數位於單獨的文件中,請不要忘記導入該函數。

這是我想通過谷歌找到的超時功能,它對我有用。

來自: 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   

@user2283347 中的方法經過測試可以正常工作,但我們希望擺脫回溯消息。 在 Ctrl-C 上使用 Remove traceback in Python 中的傳遞技巧,修改后的代碼為:

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 小時內完成。

這段代碼適用於帶有 python 3.7.3 的 Windows Server Datacenter 2016,我沒有在 Unix 上進行測試,在混合了谷歌和 StackOverflow 的一些答案之后,它終於像這樣對我有用:

from multiprocessing import Process, Lock
import time
import os

def f(lock,id,sleepTime):
    lock.acquire()
    print("I'm P"+str(id)+" Process ID: "+str(os.getpid()))
    lock.release()
    time.sleep(sleepTime)   #sleeps for some time
    print("Process: "+str(id)+" took this much time:"+str(sleepTime))
    time.sleep(sleepTime)
    print("Process: "+str(id)+" took this much time:"+str(sleepTime*2))

if __name__ == '__main__':
    timeout_function=float(9) # 9 seconds for max function time
    print("Main Process ID: "+str(os.getpid()))
    lock=Lock()
    p1=Process(target=f, args=(lock,1,6,))   #Here you can change from 6 to 3 for instance, so you can watch the behavior
    start=time.time()
    print(type(start))
    p1.start()
    if p1.is_alive():
        print("process running a")
    else:
        print("process not running a")
    while p1.is_alive():
        timeout=time.time()
        if timeout-start > timeout_function:
            p1.terminate()
            print("process terminated")
        print("watching, time passed: "+str(timeout-start) )
        time.sleep(1)
    if p1.is_alive():
        print("process running b")
    else:
        print("process not running b")
    p1.join()
    if p1.is_alive():
        print("process running c")
    else:
        print("process not running c")
    end=time.time()
    print("I am the main process, the two processes are done")
    print("Time taken:- "+str(end-start)+" secs")   #MainProcess terminates at approx ~ 5 secs.
    time.sleep(5) # To see if on Task Manager the child process is really being terminated, and it is
    print("finishing")

主要代碼來自這個鏈接: Create two child process using python(windows)

然后我使用.terminate()殺死子進程。 您可以看到函數 f 調用了 2 次打印,一次在 5 秒后,另一次在 10 秒后。 但是,使用 7 秒睡眠和終止(),它不會顯示最后一次打印。

它對我有用,希望它有幫助!

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM