如何防止Python中的KeyboardInterrupt中断代码块?

saf*_*fsd 51 python

我正在编写一个程序,通过pickle模块缓存一些结果.此刻发生的情况是,如果我在dump操作发生时按下ctrl-c,则会dump中断并且生成的文件已损坏(即只是部分写入,因此无法load再次编辑.

有没有办法制作dump,或者通常是一段代码,不间断?我目前的解决方法看起来像这样:

try:
  file = open(path, 'w')
  dump(obj, file)
  file.close()
except KeyboardInterrupt:
  file.close()
  file.open(path,'w')
  dump(obj, file)
  file.close()
  raise
Run Code Online (Sandbox Code Playgroud)

如果它被中断,重启操作似乎很愚蠢,所以我正在寻找一种推迟中断的方法.我该怎么做呢?

Gar*_*rwe 60

以下是附加信号处理程序的上下文管理器SIGINT.如果调用上下文管理器的信号处理程序,则仅在上下文管理器退出时将信号传递给原始处理程序来延迟信号.

import signal
import logging

class DelayedKeyboardInterrupt(object):
    def __enter__(self):
        self.signal_received = False
        self.old_handler = signal.signal(signal.SIGINT, self.handler)

    def handler(self, sig, frame):
        self.signal_received = (sig, frame)
        logging.debug('SIGINT received. Delaying KeyboardInterrupt.')

    def __exit__(self, type, value, traceback):
        signal.signal(signal.SIGINT, self.old_handler)
        if self.signal_received:
            self.old_handler(*self.signal_received)

with DelayedKeyboardInterrupt():
    # stuff here will not be interrupted by SIGINT
    critical_code()
Run Code Online (Sandbox Code Playgroud)

  • 尽管起初看起来有些令人生畏,但我认为这是最干净,最可重复使用的解决方案.毕竟,你只定义了一次上下文管理器(如果你愿意的话,你可以在自己的模块中轻松完成),然后你只需要一个''with''行,无论你想在哪里使用它,这都是一个为您的代码的可读性提供了很大的帮助. (7认同)
  • @benrg 哇,这是一种相当失败主义的态度。您描述的错误只会在可以轻松避免的非常模糊的情况下遇到。仅仅因为这可能并不适合所有情况,并不意味着它不适合任何情况。我真的认为你的评论没有建设性。 (6认同)
  • 很棒的课,谢谢.我将其扩展为一次支持多个信号 - 有时你还想对`SIGINTM`以及`SIGINT`做出反应:https://gist.github.com/tcwalther/ae058c64d5d9078a9f333913718bba95 (4认同)
  • **此代码有错误;不要使用它。 ** 可能是非详尽的错误列表: 1. 如果在调用 `signal` 之后但在 `__enter__` 返回之前引发异常,则该信号将被永久阻止;2. 这段代码可能会在主线程以外的线程中调用第三方异常处理程序,而CPython从来不会这样做;3. 如果`signal`返回一个不可调用的值,`__exit__`将会崩溃。@ThomasWalther 的版本部分修复了 bug 3,但至少添加了一个新 bug。Gist 上有很多类似的课程;所有这些都至少有 bug 1。我建议不要尝试修复它们——要解决这个问题太难了。 (3认同)
  • @Justin:这是因为信号处理程序只能发生在Python解释器的“原子”指令之间。(来自 https://docs.python.org/library/signal.html 的第三点) (2认同)

Unk*_*own 41

将函数放在一个线程中,等待线程完成.

除了特殊的C api之外,不能中断Python线程.

import time
from threading import Thread

def noInterrupt():
    for i in xrange(4):
        print i
        time.sleep(1)

a = Thread(target=noInterrupt)
a.start()
a.join()
print "done"


0
1
2
3
Traceback (most recent call last):
  File "C:\Users\Admin\Desktop\test.py", line 11, in <module>
    a.join()
  File "C:\Python26\lib\threading.py", line 634, in join
    self.__block.wait()
  File "C:\Python26\lib\threading.py", line 237, in wait
    waiter.acquire()
KeyboardInterrupt
Run Code Online (Sandbox Code Playgroud)

看看在线程完成之前中断是如何推迟的?

在这里它适合您的使用:

import time
from threading import Thread

def noInterrupt(path, obj):
    try:
        file = open(path, 'w')
        dump(obj, file)
    finally:
        file.close()

a = Thread(target=noInterrupt, args=(path,obj))
a.start()
a.join()
Run Code Online (Sandbox Code Playgroud)

  • 这个解决方案比涉及“signal”模块的解决方案更好,因为它更容易得到正确的解决方案。我不确定是否有可能编写一个强大的基于“信号”的解决方案。 (3认同)
  • 好吧,看来在 python 3 中中断线程后,线程不会继续打印 - 中断立即出现,但线程仍在后台继续运行 (2认同)
  • 不要使用该代码,它在 Linux 或 MacOS 上不起作用,因为它依赖于仅 Windows 的问题:当前在 Windows 上,“threading.Lock.acquire”(以及其他同步原语,例如“threading.Thread.join”) `) 不能用 Ctrl-C 中断(参见 https://bugs.python.org/issue29971)。 (2认同)

Ign*_*ams 25

使用信号模块在进程持续时间内禁用SIGINT:

s = signal.signal(signal.SIGINT, signal.SIG_IGN)
do_important_stuff()
signal.signal(signal.SIGINT, s)
Run Code Online (Sandbox Code Playgroud)

  • 如果在执行“do_important_stuff()”期间出现信号,一旦信号未被忽略,信号是否会触发? (3认同)
  • 这适用于Windows.它通过C运行时库模拟Posix信号来实现https://msdn.microsoft.com/en-us/library/xdkz3x12%28v=vs.90%29.aspx (2认同)
  • 我认为大多数时候这是最干净的解决方案。唯一的问题是当您不在主线程中工作时:“信号仅在主线程中工作”。哼。 (2认同)

Nad*_*mli 10

在我看来,使用线程是一种矫枉过正.您只需在循环中执行此操作即可确保正确保存文件,直到成功完成写入为止:

def saveToFile(obj, filename):
    file = open(filename, 'w')
    cPickle.dump(obj, file)
    file.close()
    return True

done = False
while not done:
    try:
        done = saveToFile(obj, 'file')
    except KeyboardInterrupt:
        print 'retry'
        continue
Run Code Online (Sandbox Code Playgroud)

  • 这种方法每次都会重新启动转储,这是我想要避免的一部分. (3认同)
  • +-0:这种方法不好,因为您可以通过按住crtl + c来永久中断它,而我的线程方法永远不会中断。另请注意,您必须具有另一个变量“ isinterrupted”和另一个条件语句,之后才能重新引发它。 (2认同)
  • @Unknown,@Saffsd:你们都是对的。但此解决方案适用于简单的应用程序,您预计不会被恶意使用。这是针对用户在不知情的情况下中断转储的极不可能事件的解决方法。您可以选择最适合您的应用的解决方案。 (2认同)

Bre*_*ner 5

我一直在思考对这个问题的答案的批评,我相信我已经实现了一个更好的解决方案,其使用方式如下:

with signal_fence(signal.SIGINT):
  file = open(path, 'w')
  dump(obj, file)
  file.close()
Run Code Online (Sandbox Code Playgroud)

下面是上下文管理signal_fence器,随后解释了它对之前答案的改进。该函数的文档字符串记录了其接口和保证。

import os
import signal
from contextlib import contextmanager
from types import FrameType
from typing import Callable, Iterator, Optional, Tuple
from typing_extensions import assert_never


@contextmanager
def signal_fence(
    signum: signal.Signals,
    *,
    on_deferred_signal: Callable[[int, Optional[FrameType]], None] = None,
) -> Iterator[None]:
    """
    A `signal_fence` creates an uninterruptible "fence" around a block of code. The
    fence defers a specific signal received inside of the fence until the fence is
    destroyed, at which point the original signal handler is called with the deferred
    signal. Multiple deferred signals will result in a single call to the original
    handler. An optional callback `on_deferred_signal` may be specified which will be
    called each time a signal is handled while the fence is active, and can be used
    to print a message or record the signal.

    A `signal_fence` guarantees the following with regards to exception-safety:

    1. If an exception occurs prior to creating the fence (installing a custom signal
    handler), the exception will bubble up as normal. The code inside of the fence will
    not run.
    2. If an exception occurs after creating the fence, including in the fenced code,
    the original signal handler will always be restored before the exception bubbles up.
    3. If an exception occurs while the fence is calling the original signal handler on
    destruction, the original handler may not be called, but the original handler will
    be restored. The exception will bubble up and can be detected by calling code.
    4. If an exception occurs while the fence is restoring the original signal handler
    (exceedingly rare), the original signal handler will be restored regardless.
    5. No guarantees about the fence's behavior are made if exceptions occur while
    exceptions are being handled.

    A `signal_fence` can only be used on the main thread, or else a `ValueError` will
    raise when entering the fence.
    """
    handled: Optional[Tuple[int, Optional[FrameType]]] = None

    def handler(signum: int, frame: Optional[FrameType]) -> None:
        nonlocal handled
        if handled is None:
            handled = (signum, frame)
        if on_deferred_signal is not None:
            try:
                on_deferred_signal(signum, frame)
            except:
                pass

    # https://docs.python.org/3/library/signal.html#signal.getsignal
    original_handler = signal.getsignal(signum)
    if original_handler is None:
        raise TypeError(
            "signal_fence cannot be used with signal handlers that were not installed"
            " from Python"
        )
    if isinstance(original_handler, int) and not isinstance(
        original_handler, signal.Handlers
    ):
        raise NotImplementedError(
            "Your Python interpreter's signal module is using raw integers to"
            " represent SIG_IGN and SIG_DFL, which shouldn't be possible!"
        )

    # N.B. to best guarantee the original handler is restored, the @contextmanager
    #      decorator is used rather than a class with __enter__/__exit__ methods so
    #      that the installation of the new handler can be done inside of a try block,
    #      whereas per [PEP 343](https://www.python.org/dev/peps/pep-0343/) the
    #      __enter__ call is not guaranteed to have a corresponding __exit__ call if an
    #      exception interleaves
    try:
        try:
            signal.signal(signum, handler)
            yield
        finally:
            if handled is not None:
                if isinstance(original_handler, signal.Handlers):
                    if original_handler is signal.Handlers.SIG_IGN:
                        pass
                    elif original_handler is signal.Handlers.SIG_DFL:
                        signal.signal(signum, signal.SIG_DFL)
                        os.kill(os.getpid(), signum)
                    else:
                        assert_never(original_handler)
                elif callable(original_handler):
                    original_handler(*handled)
                else:
                    assert_never(original_handler)
            signal.signal(signum, original_handler)
    except:
        signal.signal(signum, original_handler)
        raise
Run Code Online (Sandbox Code Playgroud)

首先,为什么不使用线程(接受的答案)?
在非守护线程中运行代码确实可以保证该线程将在解释器关闭时加入,但主线程上的任何异常(例如KeyboardInterrupt)都不会阻止主线程继续执行。

考虑一下如果线程方法正在使用主线程finallyKeyboardInterrupt.

其次,使用上下文管理器解决@benrg对获得最多支持的答案的反馈:

  1. 如果在调用信号之后但在__enter__返回之前引发异常,则该信号将被永久阻塞;

我的解决方案通过在@contextmanager装饰器的帮助下使用生成器上下文管理器来避免此错误。有关更多详细信息,请参阅上面代码中的完整注释。

  1. 此代码可能会在主线程以外的线程中调用第三方异常处理程序,而 CPython 绝不会这样做;

我认为这个错误不是真的。signal.signal需要从主线程调用,ValueError否则引发。这些上下文管理器只能在主线程上运行,因此只会从主线程调用第三方异常处理程序。

  1. 如果信号返回不可调用的值,__exit__将会崩溃

我的解决方案处理信号处理程序的所有可能值并适当地调用它们。此外,我还assert_never受益于静态分析器中的详尽检查。


请注意,它signal_fence旨在处理主线程上的一个中断,例如KeyboardInterrupt. 如果您的用户在恢复信号处理程序时正在发送垃圾邮件ctrlc那么没有什么可以拯救您。鉴于恢复处理程序所需执行的操作码相对较少,这种情况不太可能发生,但这是可能的。(为了获得最大的鲁棒性,该解决方案需要用C 重写