19

我正在(自制)基于 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

4

5 回答 5

17

但是,Ctrl-C 似乎没有任何效果

Ctrl-C在 shell 中发送SIGINT到前台进程组python在接收到信号时在 C 代码中设置一个标志。如果您的 C 扩展在主线程中运行,则不会运行 Python 信号处理程序(因此您不会KeyboardInterrupt在 上看到异常Ctrl-C),除非您调用PyErr_CheckSignals()它检查标志(这意味着:它不应该减慢您的速度)并运行 Python如有必要,或者您的模拟允许 Python 代码执行(例如,如果模拟使用 Python 回调),则使用信号处理程序。这是使用 @Matt 建议的 pybind11 创建的 CPython 扩展模块的代码示例

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 和 KeybordInterrupt

于 2015-11-11T14:18:59.697 回答
8

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 解释器。
于 2013-02-05T13:21:46.857 回答
4

如果您不希望您的 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。

于 2018-11-03T18:25:45.893 回答
0

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

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

延续传递风格可能与理解相关,作为一种编程风格......

于 2013-02-05T12:13:23.720 回答
0

不优雅,但我发现的唯一方法也中断了 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-12-29T15:41:14.043 回答