python装饰器限制函数运行时间,超时退出

本文介绍三种在Python中实现函数执行超时的方法:使用signal模块、使用Thread线程和使用eventlet库。这些方法可以帮助开发者在实际项目中对函数响应时间进行限制,避免长时间等待。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

实际项目中会涉及到需要对有些函数的响应时间做一些限制,如果超时就退出函数的执行,停止等待。

可以利用python中的装饰器实现对函数执行时间的控制。

python装饰器简单来说可以在不改变某个函数内部实现和原来调用方式的前提下对该函数增加一些附件的功能,提供了对该函数功能的扩展。

 

方法一. 使用 signal

使用signal有所限制,需要在linux系统上,并且需要在主线程中使用。方法二使用线程计时,不受此限制。

# coding=utf-8
import signal
import time

def set_timeout(num, callback):
    def wrap(func):
        def handle(signum, frame):  # 收到信号 SIGALRM 后的回调函数,第一个参数是信号的数字,第二个参数是the interrupted stack frame.
            raise RuntimeError

        def to_do(*args, **kwargs):
            try:
                signal.signal(signal.SIGALRM, handle)  # 设置信号和回调函数
                signal.alarm(num)  # 设置 num 秒的闹钟
                print('start alarm signal.')
                r = func(*args, **kwargs)
                print('close alarm signal.')
                signal.alarm(0)  # 关闭闹钟
                return r
            except RuntimeError as e:
                callback()

        return to_do

    return wrap

def after_timeout():  # 超时后的处理函数
    print("Time out!")

@set_timeout(2, after_timeout)  # 限时 2 秒超时
def connect():  # 要执行的函数
    time.sleep(3)  # 函数执行时间,写大于2的值,可测试超时
    print('Finished without timeout.')

if __name__ == '__main__':
    connect()

 

 

方法二. 使用Thread

Thread方法在linux和windows环境下都可以使用,不过适用于python3.3及以下版本,从python3.4版本开始Thread部分函数有更改。

 

# -*- coding: utf-8 -*-
from threading import Thread
import time

class TimeoutException(Exception):
    pass

ThreadStop = Thread._Thread__stop

def timelimited(timeout):
    def decorator(function):
        def decorator2(*args,**kwargs):
            class TimeLimited(Thread):
                def __init__(self,_error= None,):
                    Thread.__init__(self)
                    self._error =  _error

                def run(self):
                    try:
                        self.result = function(*args,**kwargs)
                    except Exception,e:
                        self._error = str(e)

                def _stop(self):
                    if self.isAlive():
                        ThreadStop(self)

            t = TimeLimited()
            t.start()
            t.join(timeout)

            if isinstance(t._error,TimeoutException):
                t._stop()
                raise TimeoutException('timeout for %s' % (repr(function)))

            if t.isAlive():
                t._stop()
                raise TimeoutException('timeout for %s' % (repr(function)))

            if t._error is None:
                return t.result

        return decorator2
    return decorator

@timelimited(2)  # 设置运行超时时间2S
def fn_1(secs):
    time.sleep(secs)
    return 'Finished without timeout'

def do_something_after_timeout():
    print('Time out!')

if __name__ == "__main__":
    try:
        print(fn_1(3))  # 设置函数执行3S
    except TimeoutException as e:
        print(str(e))
        do_something_after_timeout()

 

方法三.  使用eventlet

eventlet在python3下可用

import requests
import eventlet
import time

eventlet.monkey_patch()

time_limit = 3  #set timeout time 3s

with eventlet.Timeout(time_limit,False):
    time.sleep(5)
    r=requests.get("https://me.youkuaiyun.com/dcrmg", verify=False)
    print('error')
print('over')
<think>我们已知在Windows环境下,由于操作系统的限制,使用signal模块的SIGALRM(这是Unix特有的信号)是不可行的。因此,我们需要寻找其他方法来实现函数级别的超时控制。 根据引用[3]中提到的方法,我们可以手动计算函数耗时,并在超时时抛出异常。但这种方法不会强制终止函数,而是依赖于函数内部有检查点(比如循环中检查时间)来主动退出。如果函数内部没有这样的检查点,那么即使超时函数也会继续执行直到完成。 另一种方法是使用多进程(multiprocessing)来运行函数,然后在主进程中设置超时等待,如果子进程超时未完成,则终止子进程。这种方法可以强制停止函数的执行,但需要注意进程间通信和资源释放的问题。 下面我们分别介绍这两种方法: 方法一:使用装饰器结合多进程(推荐用于Windows,可以强制终止) 思路:将被装饰的函数放在一个独立的进程中执行,主进程等待该进程完成,若超时则终止该进程。 方法二:使用装饰器结合超时检查(需要函数内部有协作,否则无法中断) 思路:在函数内部多个关键点(如循环中)检查是否超时,如果超时则抛出异常。这种方法需要修改被装饰的函数,或者要求函数本身能在较短时间内执行完一个检查点。 由于用户要求的是在Windows环境下设置函数运行超时,且希望能够在超时后终止函数的执行,我们重点使用方法一。 下面是一个使用多进程和装饰器的实现示例: ```python import multiprocessing import time import functools from multiprocessing import TimeoutError, Process, Queue # 由于进程间通信,我们使用Queue来获取函数返回值,但注意:被装饰函数的返回值必须是可pickle的 def timeout_decorator(timeout_seconds): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): # 定义一个内部函数用于在子进程中运行目标函数,并将结果放入队列 def target(queue, *args, **kwargs): try: result = func(*args, **kwargs) queue.put(result) except Exception as e: queue.put(e) # 将异常作为结果返回,主进程会重新抛出 queue = multiprocessing.Queue() process = Process(target=target, args=(queue,)+args, kwargs=kwargs) process.start() process.join(timeout=timeout_seconds) if process.is_alive(): # 超时则终止进程 process.terminate() process.join() # 确保进程资源被回收 raise TimeoutError(f"Function {func.__name__} timed out after {timeout_seconds} seconds") else: # 获取结果 result = queue.get() if isinstance(result, Exception): raise result return result return wrapper return decorator ``` 但是,上述实现有一个问题:如果函数正常执行完成,我们通过队列获取结果;如果函数抛出异常,我们将异常放入队列,然后在主进程中重新抛出。但是,如果函数执行时间很长,我们在超时后终止了进程,那么队列中可能没有数据(因为子进程被终止,来不及放入数据),此时调用`queue.get()`会一直阻塞。为了避免这种情况,我们可以设置一个超时时间,或者使用`queue.empty()`判断。但更安全的做法是使用`join`等待进程结束后再尝试从队列中取数据(但超时后我们已经终止了进程,所以进程已经结束,但可能队列中仍然没有数据,因为子进程在放入队列前就被终止了)。因此,我们需要调整:超时后我们直接抛出超时异常,不再尝试从队列中取数据。 修改后的版本: ```python import multiprocessing import functools from multiprocessing import Process, Queue def timeout_decorator(timeout_seconds): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): # 使用队列来获取结果,但注意:如果超时,队列可能为空 queue = Queue() def target(queue, *args, **kwargs): try: result = func(*args, **kwargs) queue.put(result) except Exception as e: queue.put(e) process = Process(target=target, args=(queue,)+args, kwargs=kwargs) process.start() process.join(timeout_seconds) if process.is_alive(): process.terminate() process.join() # 等待进程终止 raise TimeoutError(f"Function {func.__name__} timed out after {timeout_seconds} seconds") else: # 进程正常结束,从队列中获取结果 if queue.empty(): # 如果队列为空,可能是函数内部没有放入任何东西(比如函数内部发生了错误但没有捕获?) # 但实际上我们的target函数捕获了所有异常,所以应该不会为空 # 但为了安全,我们可以设置一个超时来取,或者判断非空 # 这里我们直接取,如果队列为空,get会阻塞,所以我们需要设置block=False try: result = queue.get(block=False) except: raise RuntimeError("Function did not return any value") else: result = queue.get(block=False) if isinstance(result, Exception): raise result return result return wrapper return decorator ``` 但是,使用`multiprocessing.Queue`在Windows上可能会遇到一些问题,特别是在主进程和子进程之间传递复杂对象时。另外,由于Windows上多进程的实现方式(spawn),需要确保被包装的函数可以被pickle,并且注意在`if __name__ == '__main__'`的保护下使用。 另外,由于在Windows上,子进程的导入行为会导致模块被重新导入,因此需要将装饰器放在不会导致循环导入的位置。 考虑到上述实现的复杂性,还有一种更简单的方法是使用`concurrent.futures`模块,它提供了更高级的接口。 方法三:使用concurrent.futures.ProcessPoolExecutor 我们可以这样实现装饰器: ```python import concurrent.futures import functools def timeout_decorator(timeout_seconds): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): with concurrent.futures.ProcessPoolExecutor(max_workers=1) as executor: future = executor.submit(func, *args, **kwargs) try: result = future.result(timeout=timeout_seconds) return result except concurrent.futures.TimeoutError: # 取消任务(实际上是终止进程) future.cancel() raise TimeoutError(f"Function {func.__name__} timed out after {timeout_seconds} seconds") return wrapper return decorator ``` 这种方法更简洁,而且利用了标准库的高级API。但是,它同样要求函数参数和返回值是可pickle的。另外,每次调用函数都会创建一个进程池(虽然只有一个进程),但开销相对较大。 考虑到性能,如果函数调用频繁,可能不适合。但对于超时时间较长或者函数本身执行时间较长的任务,这种开销可以接受。 另外,需要注意的是,在Windows上使用多进程,必须将代码放在`if __name__ == '__main__':`块中,否则可能会引发RuntimeError。 示例使用: ```python @timeout_decorator(5) def my_function(): time.sleep(10) return "完成" if __name__ == '__main__': try: print(my_function()) except TimeoutError as e: print(e) ``` 如果函数执行超过5秒,则会抛出TimeoutError。 方法四:使用线程超时(注意:Python中线程无法被强制终止) 在Python中,由于GIL的存在,使用线程超时并强制终止线程是不安全的,因为可能会造成资源未释放或内部状态不一致。因此,不推荐使用线程终止。 总结:在Windows环境下,推荐使用多进程的方式来实现函数超时控制,并选择`concurrent.futures`模块来简化代码。 但是,请注意:使用多进程意味着函数将在独立的进程中运行,因此函数内部的状态改变不会影响主进程。同时,函数参数和返回值必须可序列化(pickleable)。 如果函数不需要强制终止,或者可以在函数内部加入超时检查,那么可以使用引用[3]中的方法,即手动计时并检查。例如: ```python import time def timeit(func, timeout): start = time.time() result = func() elapsed = time.time() - start if elapsed > timeout: raise TimeoutError(f"Function took {elapsed} seconds, longer than {timeout} seconds") return result # 使用 def my_function(): time.sleep(10) return "完成" try: result = timeit(my_function, 5) except TimeoutError as e: print(e) ``` 这种方法不会中断函数的执行,所以如果函数执行时间很长,它还是会执行完,然后抛出超时异常。因此,它适用于能够接受函数执行完但需要知道是否超时的情况,或者函数本身可以在内部检查超时(比如在循环中检查时间)并主动退出。 根据用户需求,如果希望在超时后立即终止函数的执行,则必须使用多进程方法。 因此,我们给出两种方案供选择: 方案A(多进程装饰器,强制终止): - 优点:可以强制终止超时函数 - 缺点:进程创建和通信开销大,参数和返回值需要可序列化 方案B(手动计时,不终止): - 优点:简单,无额外开销 - 缺点:无法中断函数函数会一直执行完 用户可以根据实际需求选择。 最后,我们提供方案A的装饰器实现(使用concurrent.futures)作为推荐: ```python import concurrent.futures import functools def timeout(timeout_seconds): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): with concurrent.futures.ProcessPoolExecutor(max_workers=1) as executor: future = executor.submit(func, *args, **kwargs) try: result = future.result(timeout=timeout_seconds) return result except concurrent.futures.TimeoutError: future.cancel() raise TimeoutError(f"Function {func.__name__} timed out after {timeout_seconds} seconds") return wrapper return decorator ``` 注意:在Windows上,使用此装饰器时,主模块必须被保护在`if __name__ == '__main__':`中,以避免子进程重新执行主模块代码。 另外,由于创建进程的开销,此装饰器不适用于非常高频的调用。 如果用户需要高频调用,且函数本身可以拆分,建议在函数内部实现超时检查机制(如定时检查剩余时间),这样可以在不创建新进程的情况下实现超时控制。 相关问题: 1. 如何在Windows上使用Python实现函数超时且不依赖多进程? 2. 使用多进程超时装饰器时,如何避免Windows上的进程创建开销? 3. 如何确保被终止的进程不会留下资源泄漏(如打开的文件、网络连接等)?</think>### Windows环境下设置函数运行超时时间的解决方案 在Windows系统中,由于不支持Unix信号机制(如SIGALRM),需要采用不同的方法实现函数超时控制。以下是几种可靠的方法: #### 方法1:使用多进程强制终止(推荐) ```python import multiprocessing import time import functools def timeout_decorator(timeout_seconds): """Windows兼容的函数超时装饰器""" def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): # 使用队列获取函数返回值 result_queue = multiprocessing.Queue() # 定义目标函数 def target(): try: result = func(*args, **kwargs) result_queue.put(result) except Exception as e: result_queue.put(e) # 创建并启动进程 process = multiprocessing.Process(target=target) process.start() process.join(timeout=timeout_seconds) if process.is_alive(): process.terminate() # 强制终止进程 process.join() raise TimeoutError(f"函数执行超时 ({timeout_seconds}秒)") # 获取结果或异常 result = result_queue.get() if isinstance(result, Exception): raise result return result return wrapper return decorator # 使用示例 @timeout_decorator(3) # 设置3秒超时 def long_running_task(): time.sleep(5) # 模拟耗时操作 return "任务完成" try: print(long_running_task()) except TimeoutError as e: print(e) # 输出: 函数执行超时 (3秒) ``` #### 方法2:使用线程+手动超时检查(轻量级) ```python import time import threading import functools def timeout_checker(timeout_seconds): """需要函数内部协作的超时控制""" def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): # 共享状态变量 timeout_flag = [False] result_container = [None] # 目标函数 def target(): try: result_container[0] = func(*args, **kwargs) except Exception as e: result_container[0] = e # 超时监控线程 def monitor(): time.sleep(timeout_seconds) timeout_flag[0] = True # 启动工作线程和监控线程 worker = threading.Thread(target=target) watcher = threading.Thread(target=monitor) worker.start() watcher.start() # 等待工作线程完成 while worker.is_alive(): if timeout_flag[0]: raise TimeoutError(f"函数执行超时 ({timeout_seconds}秒)") time.sleep(0.1) # 降低CPU占用 # 返回结果或异常 if isinstance(result_container[0], Exception): raise result_container[0] return result_container[0] return wrapper return decorator ``` #### 方法3:使用concurrent.futures(简洁API) ```python from concurrent.futures import ProcessPoolExecutor, TimeoutError import functools def future_timeout(timeout_seconds): """使用进程池实现的超时控制""" def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): with ProcessPoolExecutor(max_workers=1) as executor: future = executor.submit(func, *args, **kwargs) try: return future.result(timeout=timeout_seconds) except TimeoutError: future.cancel() # 取消任务 raise TimeoutError(f"函数执行超时 ({timeout_seconds}秒)") return wrapper return decorator ``` ### 关键注意事项 1. **资源清理**:强制终止进程时,可能无法正确释放资源(如打开的文件、网络连接),建议在函数中使用`try/finally`进行清理[^2] 2. **可序列化要求**:多进程方法要求函数参数和返回值必须可序列化(picklable) 3. **性能开销**:创建新进程约需10-100ms,高频调用应考虑其他方案 4. **替代方案**:对于I/O密集型任务,可使用`asyncio.wait_for`实现协程级超时 ```python import asyncio async def my_task(): await asyncio.sleep(5) try: result = asyncio.run(asyncio.wait_for(my_task(), timeout=3)) except asyncio.TimeoutError: print("协程任务超时") ``` ### 最佳实践建议 1. 优先使用多进程方案(方法1),确保可靠终止 2. 在函数内部添加超时检查点(适用于长时间循环任务): ```python def long_operation(timeout): start = time.time() for i in range(1000000): # 每100次迭代检查超时 if i % 100 == 0 and (time.time() - start) > timeout: raise TimeoutError("操作超时") # ... 正常处理逻辑 ``` > 在Windows环境下,多进程方案是最可靠的强制超时机制,但需注意资源清理和序列化限制[^1][^2]。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值