【问题标题】:Allowing Ctrl-C to interrupt a python C-extension允许 Ctrl-C 中断 python C 扩展
【发布时间】:2021-05-19 22:02:10
【问题描述】:

我正在(自制)基于 C 的 python 扩展中运行一些计算量很大的模拟。有时我会出错并想终止模拟。但是,Ctrl-C 似乎没有任何效果(除了将^C 打印到屏幕上),所以我必须使用kill 或系统监视器来终止进程。

据我所知,python 只是等待 C 扩展完成,在此期间并没有真正与它通信。

有没有办法让它工作?

更新:主要答案(针对我的具体问题)结果是: 1. 重写代码以定期将控制权交还给调用者(在下面回答Allowing Ctrl-C to interrupt a python C-extension),或者 2.使用PyErr_CheckSignals()(下面回答https://stackoverflow.com/a/33652496/423420

【问题讨论】:

标签: python linux python-c-api


【解决方案1】:

但是,Ctrl-C 似乎没有任何作用

Ctrl-C in the shell sends SIGINT to the foreground process grouppython 在接收到信号时在 C 代码中设置一个标志。如果您的 C 扩展在主线程中运行,则不会运行 Python 信号处理程序(因此您不会在 Ctrl-C 上看到 KeyboardInterrupt 异常),除非您调用检查标志的 PyErr_CheckSignals()(这意味着:它应该'不要减慢你的速度)并在必要时运行 Python 信号处理程序,或者如果你的模拟允许 Python 代码执行(例如,如果模拟使用 Python 回调)。这是code example of an extension module for CPython created using pybind11 suggested by @Matt

PYBIND11_MODULE(example, m)
{
    m.def("long running_func", []()
    {
        for (;;) {
            if (PyErr_CheckSignals() != 0)
                throw py::error_already_set();
            // Long running iteration
        }
    });
}

如果扩展在后台线程中运行,那么释放 GIL 就足够了(以允许 Python 代码在使信号处理程序运行的主线程中运行)。 PyErr_CheckSignals() 总是在后台线程中返回 0

相关:Cython, Python and KeybordInterrupt ingored

【讨论】:

  • 是否有一种纯 python 方法来检测回调中的 SIGINT,main.py -> 调用共享库 -> callback.py 的长期运行的 scipy 优化器? (我要问一个新问题吗?)
  • @denis: "pure-python way to detection SIGINT" -> KeyboardInterrupt 在主线程中引发(使用默认信号处理程序)。
  • 这应该是公认的答案。如果您正在执行不可避免的阻塞 I/O,您可能必须等待条件变量或其他东西,这是有正当理由的。你不能总是围绕它重新架构。
  • 我发现这很有帮助:pybind11.readthedocs.io/en/stable/…
【解决方案2】:

Python 在SIGINT 上安装了一个信号处理程序,它只是设置一个由主解释器循环检查的标志。为了让这个处理程序正常工作,Python 解释器必须运行 Python 代码。

您有几种选择:

  1. 使用Py_BEGIN_ALLOW_THREADS/Py_END_ALLOW_THREADS 释放您的C 扩展代码周围的GIL。不持有 GIL 时不能使用任何 Python 函数,但 Python 代码(和其他 C 代码)可以与 C 线程并发运行(真正的多线程)。一个单独的 Python 线程可以与 C 扩展一起执行并捕获 Ctrl+C 信号。
  2. 设置您自己的 SIGINT 处理程序并调用原始 (Python) 信号处理程序。然后,您的 SIGINT 处理程序可以执行所需的任何操作来取消 C 扩展代码并将控制权返回给 Python 解释器。
【解决方案3】:

如果您不希望您的 C 扩展(或 ctypes DLL)与 Python 绑定,则有另一种方法可以解决此问题,例如您想创建一个具有多种语言绑定的 C 库,您必须允许您的 C 扩展运行很长时间,并且您可以修改 C 扩展:

在 C 扩展中包含信号头。

#include <signal.h>

在 C 扩展中创建信号处理程序 typedef。

typedef void (*sighandler_t)(int);

在 C 扩展中添加信号处理程序,以执行中断任何长时间运行的代码所需的操作(设置停止标志等),并保存现有的 Python 信号处理程序。

sighandler_t old_sig_int_handler = signal(SIGINT, your_sig_handler);
sighandler_t old_sig_term_handler = signal(SIGTERM, your_sig_handler);

只要 C 扩展返回,就恢复现有的信号处理程序。此步骤可确保重新应用 Python 信号处理程序。

signal(SIGINT, old_sig_int_handler);
signal(SIGTERM, old_sig_term_handler);

如果长时间运行的代码被中断(标志等),则将控制权返回给 Python,返回代码指示信号编号。

return SIGINT;

在 Python 中,发送在 C 扩展中接收到的信号。

import os
import signal

status = c_extension.run()

if status in [signal.SIGINT, signal.SIGTERM]:
    os.kill(os.getpid(), status)

Python 将执行您期望的操作,例如为 SIGINT 引发 KeyboardInterrupt。

【讨论】:

【解决方案4】:

我会重新设计 C 扩展,使它们不会运行很长时间。

因此,将它们拆分为更基本的步骤(每个运行时间较短,例如 10 到 50 毫秒),并让这些更基本的步骤由 Python 代码调用。

continuation passing style 可能与理解相关,作为一种编程风格...

【讨论】:

  • 抱歉,在这种情况下根本不是一个选项 :) 这是一个包含大量步骤的模拟,速度至关重要。在每一步(甚至定期)与 python 交互会破坏效率。
  • 尝试将这些步骤组合成几毫秒的时间。那么使用 Python 的开销可以忽略不计……
  • 这当然值得考虑,但它确实会引发很多内存管理等问题。谢谢!
  • 还没有实现,但这绝对在我的待办事项清单上。再次感谢!
【解决方案5】:

不优雅,但我发现的唯一方法也中断了 C++ 中的外部库调用并终止所有正在运行的子进程。

#include <csignal>
#include <pybind11/pybind11.h>

void catch_signals() {
  auto handler = [](int code) { throw std::runtime_error("SIGNAL " + std::to_string(code)); };
  signal(SIGINT, handler);
  signal(SIGTERM, handler);
  signal(SIGKILL, handler);
}

PYBIND11_MODULE(example, m)
{
    m.def("some_func", []()
    {
        catch_signals();
        // ...
    });
}
import sys

from example import some_func

try:
    some_func()
except RuntimeError as e:
    if "SIGNAL" in str(e):
        code = int(str(e).rsplit(" ", 1)[1])
        sys.exit(128 + code)
    raise

【讨论】:

    猜你喜欢
    • 2021-09-27
    • 1970-01-01
    • 2016-01-19
    • 1970-01-01
    • 2018-04-02
    • 1970-01-01
    • 2019-12-09
    • 1970-01-01
    相关资源
    最近更新 更多