超时控制装饰器和上下文管理器,在另一个线程中抛出任何异常
项目描述
在另一个线程中引发异步异常,使用两个上下文管理器和两个装饰器控制块或可调用的超时。
概述
本模块提供
一个在另一个线程中引发异常的函数,包括主线程。
两个可能因为超时而停止其内部块活动的上下文管理器。
两个可能因为超时而停止其装饰函数的装饰器。
在MacOSX上使用CPython 2.6、2.7、3.3和3.4开发和测试。应在任何操作系统(xBSD、Linux、Windows)上工作,除非明确说明。
安装
在您的应用程序中使用stopit
它们的工作方式相同
easy_install stopit
pip install stopit
开发stopit
# You should prefer forking if you have a Github account
git clone https://github.com/glenfant/stopit.git
cd stopit
python setup.py develop
# Does it work for you ?
python setup.py test
公共API
异常
stopit.TimeoutException
stopit.TimeoutException可能在超时上下文管理器控制的块中引发。
此异常可能在上下文管理器控制的块执行结束时在您的应用程序中传播,请参阅上下文管理器的swallow_ex参数。
请注意,在用xxx_timeoutable(...)装饰的函数执行后,始终会吞下stopit.TimeoutException。无论如何,您可以在装饰函数内部捕获此异常。
基于线程的资源
stopit.async_raise
在另一个线程中引发任意异常的函数
async_raise(tid, exception)
tid是线程标识符,由线程对象的ident属性提供。有关更多信息,请参阅threading模块的文档。
exception是要在线程中引发的异常类或对象。
stopit.ThreadingTimeout
一个“杀死”其内部块执行超过提供时间的上下文管理器。
ThreadingTimeout(seconds, swallow_exc=True)
seconds是上下文管理块执行允许的秒数。
swallow_exc:如果为False,则在退出上下文管理块时将重新引发可能的stopit.TimeoutException。注意:值为True不会吞下其他潜在的异常。
方法和属性
属于stopit.ThreadingTimeout上下文管理器的。
方法/属性 |
描述 |
---|---|
.cancel() |
取消超时控制。此方法旨在在超时控制的块内使用,特别是用于取消超时控制。这意味着此调用之后的所有代码都可能执行到结束。 |
.state |
此属性指示超时控制的实际状态。它可能具有EXECUTED、EXECUTING、TIMED_OUT、INTERRUPTED或CANCELED属性之一。请参阅以下内容。 |
.EXECUTING |
超时控制正在执行中。我们通常在上下文管理器控制的代码中执行。 |
.EXECUTED |
好消息:在分配的时间框架内,超时控制的代码已正常完成。 |
.TIMED_OUT |
坏消息:受超时控制的代码已睡眠时间过长。应在超时控制的块内创建或更改的对象应被视为不存在或损坏。请勿随意操作它们。 |
.已中断 |
受超时控制的代码可能因任何应用逻辑原因而自行引发显式的stopit.TimeoutException。这种有意退出的情况可以通过此状态值从超时控制的块外部检测到。 |
.已取消 |
超时控制已有意取消,受超时控制的代码已正常运行完成。但可能超过了分配的时间框架。 |
典型用法
import stopit
# ...
with stopit.ThreadingTimeout(10) as to_ctx_mgr:
assert to_ctx_mgr.state == to_ctx_mgr.EXECUTING
# Something potentially very long but which
# ...
# OK, let's check what happened
if to_ctx_mgr.state == to_ctx_mgr.EXECUTED:
# All's fine, everything was executed within 10 seconds
elif to_ctx_mgr.state == to_ctx_mgr.EXECUTING:
# Hmm, that's not possible outside the block
elif to_ctx_mgr.state == to_ctx_mgr.TIMED_OUT:
# Eeek the 10 seconds timeout occurred while executing the block
elif to_ctx_mgr.state == to_ctx_mgr.INTERRUPTED:
# Oh you raised specifically the TimeoutException in the block
elif to_ctx_mgr.state == to_ctx_mgr.CANCELED:
# Oh you called to_ctx_mgr.cancel() method within the block but it
# executed till the end
else:
# That's not possible
请注意,上下文管理器对象可以被视为一个布尔值,表示(如果True)块已正常执行
if to_ctx_mgr:
# Yes, the code under timeout control completed
# Objects it created or changed may be considered consistent
stopit.threading_timeoutable
一个装饰器,如果被装饰的函数或方法在给定的时间框架内没有返回,则会将其终止。
stopit.threading_timeoutable([default [, timeout_param]])
default 是当被装饰的函数或方法执行超时时应返回的值,以通知调用代码该函数未在分配的时间框架内完成。
如果未提供此参数,则被装饰的函数或方法在执行超时时将返回一个 None 值。
@stopit.threading_timeoutable(default='not finished') def infinite_loop(): # As its name says... result = infinite_loop(timeout=5) assert result == 'not finished'
timeout_param:您装饰的函数或方法可能需要以任何原因命名的 timeout 参数。这使您能够将装饰函数签名中的 timeout 参数名称更改为任何适合的名称,并防止潜在的命名冲突。
@stopit.threading_timeoutable(timeout_param='my_timeout') def some_slow_function(a, b, timeout='whatever'): # As its name says... result = some_slow_function(1, 2, timeout="something", my_timeout=2)
关于装饰函数
或方法…
如您所注意到的,您只需在调用函数或方法时添加 timeout 参数。或者使用装饰器的 timeout_param 选择的其他名称。在调用实际的内部函数或方法时,此参数将被删除。
基于信号的资源
stopit.SignalTimeout 和 stopit.signal_timeoutable 与各自的基于线程的资源具有完全相同的 API,即 stopit.ThreadingTimeout 和 stopit.threading_timeoutable。
日志记录
名为 stopit 的日志记录器在代码执行块超过相关的超时时会发出警告。要关闭日志记录,只需
import logging
stopit_logger = logging.getLogger('stopit')
stopit_logger.seLevel(logging.ERROR)
比较基于线程和基于信号的超时控制
特性 |
基于线程的资源 |
基于信号的资源 |
---|---|---|
GIL |
无法中断长Python原子指令。例如,如果 time.sleep(20.0) 实际正在执行,则超时将在此行的执行结束时生效。 |
不要关心它 |
线程安全 |
是:只要每个线程使用自己的 ThreadingTimeout 上下文管理器或 threading_timeoutable 装饰器,就是线程安全的。 |
不是线程安全的。在多线程应用程序中可能会产生不可预测的结果。 |
可嵌套的上下文管理器 |
是:您可以嵌套基于线程的上下文管理器 |
不是:永远不要在另一个上下文管理器中嵌套基于信号的上下文管理器。最内层的上下文管理器将自动取消外部上下文管理器的超时控制。 |
精度 |
任何正浮点值都可以作为超时值。准确性取决于您平台上的 GIL 间隔检查。有关您 Python 版本的说明,请参阅 sys.getcheckinterval 和 sys.setcheckinterval 文档。 |
由于使用了 signal.SIGALRM,我们需要提供整数秒数。因此,0.6 秒的超时将被自动转换为零秒的超时! |
支持的平台 |
任何 OS 上支持线程的 CPython 2.6、2.7 或 3.3。 |
任何支持 signal.SIGALRM 的 Python 2.6、2.7 或 3.3。这不包括 Windows 系统 |
已知问题
超时精度
重要:CPython 对线程和异步特性的支持方式会影响超时的准确性。换句话说,如果您将 2.0 秒的超时分配给上下文管理块或装饰器调用的对象,实际代码块/可调用对象的执行中断可能发生在分配超时后的几秒钟。
有关此问题的更多背景信息(无法修复)——请阅读有关 Python 线程、GIL 和上下文切换的 Python 大师的观点,例如以下这些
这就是为什么我在接下来可以阅读的测试中对超时准确性的“宽容度”比在关键实时应用程序(这不在 Python 范围内)中应有的宽容度要高。
无论如何,可以通过减少默认为 100 的检查间隔来以牺牲全局性能为代价来提高准确性。参见
https://docs.pythonlang.cn/2.7/library/sys.html#sys.getcheckinterval
https://docs.pythonlang.cn/2.7/library/sys.html#sys.getcheckinterval
如果这对用户来说是一个真正的问题(想要精确的超时而不是近似值),未来的版本将向上下文管理器和装饰器添加可选的 check_interval 参数。此参数将允许在上下文管理块或装饰函数执行期间暂时降低线程切换检查间隔,以获得更准确的超时,但会牺牲整体性能。
gevent 支持
在 gevent 工作进程的上下文中使用时,如 基于线程的资源 中提到的线程超时控制不起作用。
有关更多详细信息,请参阅 问题 13 的讨论。
测试和演示
>>> import threading
>>> from stopit import async_raise, TimeoutException
在实际应用程序中,您应使用以下任一方法
>>> from stopit import ThreadingTimeout as Timeout, threading_timeoutable as timeoutable #doctest: +SKIP
或 POSIX 信号基于的资源
>>> from stopit import SignalTimeout as Timeout, signal_timeoutable as timeoutable #doctest: +SKIP
让我们定义一些实用程序
>>> import time
>>> def fast_func():
... return 0
>>> def variable_duration_func(duration):
... t0 = time.time()
... while True:
... dummy = 0
... if time.time() - t0 > duration:
... break
>>> exc_traces = []
>>> def variable_duration_func_handling_exc(duration, exc_traces):
... try:
... t0 = time.time()
... while True:
... dummy = 0
... if time.time() - t0 > duration:
... break
... except Exception as exc:
... exc_traces.append(exc)
>>> def func_with_exception():
... raise LookupError()
async_raise 函数在另一个线程中引发异常
使用 5 秒的线程测试 async_raise()
>>> five_seconds_threads = threading.Thread(
... target=variable_duration_func_handling_exc, args=(5.0, exc_traces))
>>> start_time = time.time()
>>> five_seconds_threads.start()
>>> thread_ident = five_seconds_threads.ident
>>> five_seconds_threads.is_alive()
True
我们在那个线程中引发一个 LookupError
>>> async_raise(thread_ident, LookupError)
好吧,但我们必须等待几毫秒线程死亡,因为异常是异步的
>>> while five_seconds_threads.is_alive():
... pass
并且我们可以注意到我们在线程自行停止之前停止了线程
>>> time.time() - start_time < 0.5
True
>>> len(exc_traces)
1
>>> exc_traces[-1].__class__.__name__
'LookupError'
Timeout 上下文管理器
上下文管理器在给定时间后停止其内部块的执行。您可以使用 try: ... except: ... 构造或检查块之后的上下文管理器 state 属性来管理超时发生的方式。
吞咽超时异常
我们检查快速函数在上下文管理器外部返回
>>> with Timeout(5.0) as timeout_ctx:
... result = fast_func()
>>> result
0
>>> timeout_ctx.state == timeout_ctx.EXECUTED
True
并且上下文管理器被认为是 True(块执行了其最后一行)
>>> bool(timeout_ctx)
True
我们检查慢速函数被中断
>>> start_time = time.time()
>>> with Timeout(2.0) as timeout_ctx:
... variable_duration_func(5.0)
>>> time.time() - start_time < 2.2
True
>>> timeout_ctx.state == timeout_ctx.TIMED_OUT
True
并且由于块超时,上下文管理器被认为是 False
>>> bool(timeout_ctx)
False
其他异常会被传播,必须像通常一样处理
>>> try:
... with Timeout(5.0) as timeout_ctx:
... result = func_with_exception()
... except LookupError:
... result = 'exception_seen'
>>> timeout_ctx.state == timeout_ctx.EXECUTING
True
>>> result
'exception_seen'
传播TimeoutException
我们还可以选择传播 TimeoutException。必须处理潜在异常
>>> result = None
>>> start_time = time.time()
>>> try:
... with Timeout(2.0, swallow_exc=False) as timeout_ctx:
... variable_duration_func(5.0)
... except TimeoutException:
... result = 'exception_seen'
>>> time.time() - start_time < 2.2
True
>>> result
'exception_seen'
>>> timeout_ctx.state == timeout_ctx.TIMED_OUT
True
还必须处理其他异常
>>> result = None
>>> start_time = time.time()
>>> try:
... with Timeout(2.0, swallow_exc=False) as timeout_ctx:
... func_with_exception()
... except Exception:
... result = 'exception_seen'
>>> time.time() - start_time < 0.1
True
>>> result
'exception_seen'
>>> timeout_ctx.state == timeout_ctx.EXECUTING
True
timeoutable 可调用装饰器
此装饰器会停止执行任何不应持续一定时间的可调用对象。
如果您没有提供timeout可选参数,则可以使用没有超时控制的装饰函数。
>>> @timeoutable()
... def fast_double(value):
... return value * 2
>>> fast_double(3)
6
您可以使用timeout可选参数来指定超时。被中断的函数返回None。
>>> @timeoutable()
... def infinite():
... while True:
... pass
... return 'whatever'
>>> infinite(timeout=1) is None
True
或者提供给timeoutable装饰器参数的任何其他值。
>>> @timeoutable('unexpected')
... def infinite():
... while True:
... pass
... return 'whatever'
>>> infinite(timeout=1)
'unexpected'
如果timeout参数名可能与您的函数签名冲突,您可以使用timeout_param来更改它。
>>> @timeoutable('unexpected', timeout_param='my_timeout')
... def infinite():
... while True:
... pass
... return 'whatever'
>>> infinite(my_timeout=1)
'unexpected'
它也适用于实例方法。
>>> class Anything(object):
... @timeoutable('unexpected')
... def infinite(self, value):
... assert type(value) is int
... while True:
... pass
>>> obj = Anything()
>>> obj.infinite(2, timeout=1)
'unexpected'
链接
致谢
这是一个NIH(Not Invented Here,意为“非原创”)包,主要是我从Gabriel Ahtune的食谱中借鉴的,包括测试、一些改进和重构、文档以及setuptools支持。我因为厌倦了在需要超时控制的各个项目中复制粘贴这个食谱而制作了它。
Gilles Lenfant:包创建者和维护者。
许可
此软件为开源软件,根据MIT许可证条款提供。请参阅此存储库中的LICENSE文件。
变更日志
1.1.2 - 2018-02-09
已更改许可证为MIT
在Python 3.5和3.6上进行了测试
1.1.1 - 2015-03-22
修复了Python 2.x下超时上下文管理器作为bool的bug
在Python 3.4上进行了测试
1.1.0 - 2014-05-02
添加了对基于TIMER信号的超时控制的支持(仅限于Posix操作系统)
由于新的超时控制而发生的API更改
详尽的文档。
1.0.0 - 2014-02-09
初始版本
项目详情
stopit-1.1.2.tar.gz的哈希
算法 | 哈希摘要 | |
---|---|---|
SHA256 | f7f39c583fd92027bd9d06127b259aee7a5b7945c1f1fa56263811e1e766996d |
|
MD5 | 0f6ef75399a9420fad7b7970d7be6d58 |
|
BLAKE2b-256 | 3558e8bb0b0fb05baf07bbac1450c447d753da65f9701f551dca79823ce15d50 |