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

嵌入Cython的C环境下PyQT5 GUI多线程冻结问题求解

问题

我用PyQt5开发GUI应用,进程内需要运行两个协作线程:线程1负责运行MainWindow主窗口,线程2(Emitter)负责监控主窗口的输入信号或变化。

相关代码片段

MainWindow(线程1)代码:

class MainWindow(QMainWindow, Ui_MainWindow):

    def __init__(self, queue_from_mother: Queue, queue_to_mother: Queue, emitter: Emitter):
        QMainWindow.__init__(self)
        self.queue_from_mother = queue_from_mother
        self.queue_to_mother = queue_to_mother
        self.emitter = emitter
        self.emitter.daemon = True
        self.emitter.start()

   # 更多无关代码省略

Emitter(线程2)代码:

class Emitter(QThread):
    def __init__(self, from_process: Pipe, queue_to_mother: Queue, queue_from_mother: Queue):
        super().__init__()

    def run(self):
        while True:
            GUIcmds = <list>ReadMemorySpace() # 读取共享内存并返回Python对象的外部C函数
            if("change" in GUIcmds):
                self.somelbl.emit("GUIcmds")

cdef public void mainSec():
    app = QtWidgets.QApplication(sys.argv)
    emitter = mdx.Emitter(mother_pipe, queue_to_mother, queue_from_mother)
    Dialog = mdx.MainWindow(queue_from_mother, queue_to_mother, emitter)
    Dialog.show()
    sys.exit(app.exec_())

外部纯C代码:

#include "mdx.h"

int main(){
     program = Py_DecodeLocale(argv[0], NULL);
if (program == NULL) {
    fprintf(stderr, "Fatal error: cannot decode argv[0], got %d arguments\n", argc);
    exit(1);
}


//PyImport_AppendInittab("mdx", PyInit_mdx);
if (PyImport_AppendInittab("commonmodules", PyInit_mdx) == -1) {
    fprintf(stderr, "Error: could not extend in-built modules table\n");
    exit(1);
}
Py_SetProgramName(program);

Py_Initialize();

pmodule = PyImport_ImportModule("mdx");
if (!pmodule) {
    PyErr_Print();
    fprintf(stderr, "Error: could not import module 'embedded'\n");
    return -1;
}

PyObject* sys = PyImport_ImportModule("sys");
PyObject* path = PyObject_GetAttrString(sys, "path");
PyList_Append(path, PyUnicode_FromString("."));
PyList_Append(path, PyUnicode_FromString("C:/Users/uesrname/AppData/Local/Programs/Python/python39/"));


/* 1st: Import the module */
PyObject* ModuleString = PyUnicode_FromString((char*)"mdx");
if (!ModuleString) {
    PyErr_Print();
    printf("Error formating python script\n");
}

PyObject* Module = PyImport_Import(ModuleString);
if (!Module) {
    PyErr_Print();
    printf("Error importing python script\n");
}
__pyx_f_13commonmodules_mainSec();

Py_Finalize();
}

当前问题:Emitter线程进入循环后,MainWindow主窗口直接冻结。添加time.sleep(0.3)后窗口恢复交互,推测是Python GIL导致的问题。需要实现Emitter线程运行时不影响主窗口交互。


解决方案

1. 给C扩展函数主动释放GIL

ReadMemorySpace()作为外部C函数,如果执行耗时较长,会一直持有GIL,导致GUI主线程无法获取GIL处理界面事件。修改C函数,在执行耗时操作前后手动释放/重新获取GIL:

// 在ReadMemorySpace函数内部
Py_BEGIN_ALLOW_THREADS
// 这里执行实际的共享内存读取操作,无需持有GIL
read_shared_memory_operation();
Py_END_ALLOW_THREADS

这样C函数执行耗时逻辑时会释放GIL,GUI线程就能抢占GIL处理界面更新,避免窗口冻结。

2. 用QThread延时替代time.sleep

PyQt的QThread提供的msleep()/usleep()方法在延时期间会主动释放GIL,比time.sleep()更适配Qt线程模型。修改Emitter的run方法:

from PyQt5.QtCore import QThread

class Emitter(QThread):
    # ... 其他代码
    def run(self):
        while True:
            GUIcmds = <list>ReadMemorySpace()
            if("change" in GUIcmds):
                self.somelbl.emit("GUIcmds")
            # 添加短延时释放GIL,时长可根据监控需求调整
            QThread.msleep(10)

3. 改成事件驱动监控替代死循环轮询

如果共享内存的变化可以触发系统事件(如Windows事件、Linux inotify),可以把轮询逻辑改成事件驱动:

  • 在C扩展中监听共享内存的变化事件,发生变化时通过回调或信号通知Python层。
  • Emitter线程等待事件触发,而非持续轮询,减少CPU占用和GIL竞争。

4. 严格遵守Qt线程的UI更新规则

Qt中子线程不能直接操作主线程UI控件,必须通过信号槽机制让主线程处理UI更新。调整Emitter和MainWindow的代码:

from PyQt5.QtCore import pyqtSignal, QThread

class Emitter(QThread):
    # 定义自定义信号,用于通知主线程更新UI
    update_ui_signal = pyqtSignal(str)

    def __init__(self, from_process: Pipe, queue_to_mother: Queue, queue_from_mother: Queue):
        super().__init__()

    def run(self):
        while True:
            GUIcmds = <list>ReadMemorySpace()
            if("change" in GUIcmds):
                # 发送信号,而非直接操作UI
                self.update_ui_signal.emit(str(GUIcmds))
            QThread.msleep(10)

# 在MainWindow中连接信号到UI更新槽函数
class MainWindow(QMainWindow, Ui_MainWindow):
    def __init__(self, queue_from_mother: Queue, queue_to_mother: Queue, emitter: Emitter):
        QMainWindow.__init__(self)
        # ... 其他初始化代码
        emitter.update_ui_signal.connect(self.update_label)

    def update_label(self, text):
        # 在主线程中执行UI更新操作
        self.somelbl.setText(text)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 19:12:34