You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Go编写的Python扩展忽略Ctrl+C的原因及解决办法问询

问题背景与疑问

我在用Go编写的Python驱动连接服务时,遇到了一个问题:当服务实例处于不健康状态时,驱动会陷入停滞,只能通过kill进程终止。经过测试发现两个明显差异:

  • 执行Go编写的扩展时,程序会忽略Ctrl+C指令,必须等到Go函数执行完毕、控制权回到Python后才会响应;
  • 同样逻辑的C编写扩展,执行时按下Ctrl+C会立刻触发KeyboardInterrupt异常。

我想搞清楚:

  1. 为什么会出现这种差异?
  2. 有没有办法规避这个问题,或者实现类似超时的机制?(之前试过用threading.Timer抛出异常模拟超时,但异常是在Timer线程里抛出的,无法中断主线程的Go函数执行)

测试环境:CPython 3.9/3.10,Go 1.20.4,C编译器9.4.0

原因分析

本质差异来自CPython和Go runtime的信号处理逻辑冲突:

  • CPython的信号处理机制:CPython默认会把Ctrl+C对应的SIGINT信号转化为KeyboardInterrupt异常,但这个转换是在Python字节码解释器的主循环中完成的。当调用C扩展的阻塞函数时,CPython会保留信号处理逻辑,而像sleep这类标准C函数在收到SIGINT时会直接中断并返回错误,此时CPython会检测到信号并抛出异常。
  • Go runtime的信号接管:Go程序有独立的runtime和信号处理系统,当Go编译成共享库被Python加载后,Go的runtime会接管SIGINT等信号的处理。此时Python的信号处理逻辑被覆盖,Ctrl+C发送的SIGINT会被Go捕获,不会传递给Python解释器,直到Go函数执行完毕。另外,Go的time.Sleep是由runtime调度的,不会被外部信号直接中断,所以就算按了Ctrl+C,Go的sleep也会执行到结束。
解决方案

针对这个问题,有几种可行的解决思路:

1. 在Go扩展中显式处理SIGINT信号

修改Go代码,主动监听SIGINT信号,收到信号时提前退出阻塞操作。这样Python就能在按下Ctrl+C时立刻终止Go函数的执行:

package main

import (
    "C"
    "log"
    "os"
    "os/signal"
    "syscall"
    "time"
)

// 必须加//export注释,否则Python无法正确调用该函数
//export helloWorld
func helloWorld() {
    log.Println("Hello World")
    
    // 创建信号通道,监听SIGINT
    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, syscall.SIGINT)
    
    // 用select同时等待sleep完成或信号到来
    select {
    case <-time.After(10 * time.Second):
        log.Println("Done sleeping")
    case <-sigChan:
        log.Println("Received SIGINT, exiting early")
        signal.Stop(sigChan)
        return
    }
}

func main() {}

编译命令:go build -buildmode=c-shared -o go_library.so

2. 将Go逻辑改为独立进程调用

把Go代码编译成可执行文件,Python通过subprocess模块调用,这样可以直接向子进程发送信号来中断执行,避免主线程被阻塞:

import subprocess
import ctypes

try:
    print("Calling golang executable")
    proc = subprocess.Popen(["./go_executable"])
    proc.wait()
    
    print("Calling C code")
    c_library = ctypes.cdll.LoadLibrary("./c_library.so")
    hello_c = c_library.helloWorld
    hello_c()
except KeyboardInterrupt:
    print("Ctrl+C issued")
    if 'proc' in locals():
        proc.send_signal(subprocess.signal.SIGINT)
finally:
    print("Done")

Go代码编译为可执行文件:go build -o go_executable

3. 用信号实现超时机制(改进版)

之前的threading.Timer无法中断主线程,是因为Python线程是协作式的。可以用signal模块在主线程设置超时信号,直接触发异常:

import ctypes
import signal

def timeout_handler(signum, frame):
    raise TimeoutError("Function execution timed out")

go_library = ctypes.cdll.LoadLibrary('./go_library.so')
hello_Go = go_library.helloWorld

c_library = ctypes.cdll.LoadLibrary("./c_library.so")
hello_c = c_library.helloWorld

try:
    # 设置5秒超时
    signal.signal(signal.SIGALRM, timeout_handler)
    signal.alarm(5)
    
    print("Calling golang code")
    hello_Go()
    signal.alarm(0)  # 执行完成,取消超时
    
    print("Calling C code")
    hello_c()
except (KeyboardInterrupt, TimeoutError) as e:
    print(f"Interrupt/Timeout occurred: {e}")
finally:
    signal.alarm(0)  # 确保超时被取消
    print("Done")

注意:这种方法需要Go扩展配合处理信号,否则超时后Go函数可能还在后台运行,会导致资源泄漏。

测试用POC代码

Python调用代码

import ctypes

go_library = ctypes.cdll.LoadLibrary('./go_library.so')
hello_Go = go_library.helloWorld

c_library = ctypes.cdll.LoadLibrary("./c_library.so")
hello_c = c_library.helloWorld

try:
    print("Calling golang code")
    hello_Go()

    print("Calling C code")
    hello_c()
except KeyboardInterrupt:
    print("Ctrl+C issued:")
finally:
    print("Done")

Go扩展原代码

package main

import (
    "C"
    "log"
    "time"
)

func helloWorld(){
    log.Println("Hello World")
    time.Sleep(10 * time.Second)
    log.Println("Done sleeping")
}

func main(){
}

C扩展原代码

#include <stdio.h>
#include <unistd.h> // sleep函数需要的头文件

int helloWorld() {
    printf("Hello from C\n");
    sleep(10);
    printf("Done sleeping from C\n");

    return 0;
}

编译C代码命令:gcc -shared -fPIC -o c_library.so c_library.c

内容的提问来源于stack exchange,提问作者puerkito66

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.21 19:24:59