嵌入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
相关产品推荐
相关产品推荐

