PyQt6中QThreadPool无法关闭致窗口无法关闭的问题咨询
PyQt6中QThreadPool任务完成后无法关闭窗口的问题及线程池自动关闭疑问
我用PyQt6给耗时函数做加载界面,通过QThreadPool把任务放到独立线程运行,代码如下:
from src.gui.loading import Ui_Form from PyQt6.QtWidgets import QWidget from PyQt6.QtCore import QThreadPool, QRunnable, QTimer, QThread from typing import Callable class Loading(Ui_Form, QWidget): def __init__(self, parent: QWidget | None, next_widget: QWidget | None, action: str, time: int, task: Callable, task_len: int, initial_task: str): super().__init__() self.setupUi(self) self.setParent(parent) self.parent = parent self.next_widget = next_widget self.time = time self.task = TaskRunner(self, task) self.current_time = 0 self.tasks_done = 0 self.all_tasks = task_len self.Task.setText(action) self.Estimation.setText(f"estimated time: {self.int_to_time(time)}") self.progressBar.setValue(0) self.TimeLeft.setText("") self.Current.setText("") self.Task.setText("") self.thread_pool = QThreadPool(self) self.thread_pool.destroyed.connect(self.quit) self.run_tasks() self.task_done(initial_task) self.timer = QTimer(self) self.timer.timeout.connect(self.update_time) self.timer.start(1000) @staticmethod def int_to_time(time: int) -> str: if time >= 3600: return f"{time / 3600} hours" elif time >= 60: return f"{time / 60} minutes" else: return f"{time} seconds" def update_time(self): self.current_time += 1 self.TimeLeft.setText(self.int_to_time(self.current_time)) def task_done(self, next_task: str = None): self.tasks_done += 1 if not next_task: self.Current.setText("finished all tasks, closing window") self.Tasks.setText(f"{self.tasks_done} out of {self.all_tasks}") elif self.tasks_done != self.all_tasks: self.Current.setText(f"currently: {next_task}") self.Tasks.setText(f"{self.tasks_done} out of {self.all_tasks}") def quit(self): self.close() def closeEvent(self, a0): self.timer.stop() self.thread_pool.waitForDone(-1) self.close() def run_tasks(self): self.thread_pool.start(self.task) class TaskRunner(QRunnable): def __init__(self, parent: Loading | None, task: Callable): super().__init__() self.parent = parent self.task = task def run(self): self.task(self.parent) if self.parent: self.parent.task_done(None)
代码能正常运行,但调用self.close()始终关不了窗口,调试发现任务完成后self.thread_pool还没关闭。我有Rust开发经验,Rust里的线程池会在无运行任务时自动关闭,想问问PyQt6能不能实现同样的操作?如果不行,原因是什么?
附Ui_Form代码
# Form implementation generated from reading ui file 'loading.ui' # # Created by: PyQt6 UI code generator 6.6.0 # # WARNING: Any manual changes made to this file will be lost when pyuic6 is # run again. Do not edit this file unless you know what you are doing. from PyQt6 import QtCore, QtGui, QtWidgets class Ui_Form(object): def setupUi(self, Form): Form.setObjectName("Form") Form.resize(836, 302) Form.setStyleSheet("#centralwidget {background-color:rgba(20, 20, 20, 250);}\n" "#Form {background-color:rgba(20, 20, 20, 250);}\n" "QMainWindow{background-color:rgba(20, 20, 20, 250);}\n" "\n" "QWidget{\n" " color:rgba(249, 249, 249, 240);\n" "}\n" "\n" "QMenuBar{background-color:rgba(20, 20, 20, 250);}\n" "QMenu{background-color:rgba(20, 20, 20, 250);}\n" "QMenu::item:selected{background-color:rgba(229, 229, 229, 100);}\n" "QMenuBar::item:selected{background-color:rgba(229, 229, 229, 100);}\n" "QListWidget{background-color:rgba(20, 20, 20, 250);}\n" "QSpinBox{background-color:rgba(20, 20, 20, 250);}\n" "\n" "QMenu::separator{height:5px; background-color:rgba(191, 191, 191, 100);}\n" "\n" "QComboBox {\n" " color:rgba(193, 193, 193, 250);\n" " background-color:rgba(29, 29, 29, 250);\n" " border:none;\n" "}\n" "QComboBox QAbstractItemView{\n" " color:rgba(193, 193, 193, 250);\n" " background-color:rgba(29, 29, 29, 250);\n" " border:none;\n" "}\n" "QLineEdit {\n" " color:rgba(193, 193, 193, 250);\n" " background-color:rgba(29, 29, 29, 250);\n" " border:none;\n" "}\n" "QTextEdit {\n" " color:rgba(193, 193, 193, 250);\n" " background-color:rgba(29, 29, 29, 250);\n" " border:none;\n" "}\n" "QProgressBar {\n" " border: 0px solid grey;\n" " border-radius: 0px;\n" " background-color:rgba(255, 255, 255, 0);\n" " }\n" "QTableWidget {\n" " background-color: rgba(29, 29, 29, 250);\n" " color: rgba(193, 193, 193, 250);\n" "}\n" "\n" "QTableWidget::item {\n" " padding: 4px;\n" "}\n" "\n" "QTableWidget::item:selected {\n" " background-color: rgba(50, 50, 50, 250);\n" "}\n" "\n" "QTableWidget::item:focus {\n" " background-color: rgba(70, 70, 70, 250);\n" " outline: none;\n" "}\n" "\n" "QHeaderView::section {\n" " background-color: rgba(50, 50, 50, 250);\n" " color: rgba(193, 193, 193, 250);\n" " padding: 4px;\n" " border: none;\n" "}\n" "\n" "QHeaderView {\n" " background-color: rgba(50, 50, 50, 250);\n" " color: rgba(193, 193, 193, 250);\n" " padding: 4px;\n" " border: none;\n" "}\n" "\n" "QHeaderView::section:checked {\n" " background-color: rgba(70, 70, 70, 250);\n" "}\n" " QTableView QTableCornerButton::section {\n" " background-color: rgba(50, 50, 50, 250);\n" " }\n" "\n" "QPushButton{background-color:rgba(59, 59, 59, 250);}\n" "QPushButton:hover{background-color:rgba(107, 107, 107, 250);}") self.verticalLayout = QtWidgets.QVBoxLayout(Form) self.verticalLayout.setObjectName("verticalLayout") self.Task = QtWidgets.QLabel(parent=Form) font = QtGui.QFont() font.setFamily("Secular One") font.setPointSize(20) self.Task.setFont(font) self.Task.setText("") self.Task.setObjectName("Task") self.verticalLayout.addWidget(self.Task, 0, QtCore.Qt.AlignmentFlag.AlignHCenter) self.horizontalLayout = QtWidgets.QHBoxLayout() self.horizontalLayout.setObjectName("horizontalLayout") self.Estimation = QtWidgets.QLabel(parent=Form) font = QtGui.QFont() font.setFamily("Secular One") font.setPointSize(20) self.Estimation.setFont(font) self.Estimation.setObjectName("Estimation") self.horizontalLayout.addWidget(self.Estimation) self.TimeLeft = QtWidgets.QLabel(parent=Form) font = QtGui.QFont() font.setFamily("Secular One") font.setPointSize(20) self.TimeLeft.setFont(font) self.TimeLeft.setText("") self.TimeLeft.setObjectName("TimeLeft") self.horizontalLayout.addWidget(self.TimeLeft, 0, QtCore.Qt.AlignmentFlag.AlignRight) self.verticalLayout.addLayout(self.horizontalLayout) self.horizontalLayout_2 = QtWidgets.QHBoxLayout() self.horizontalLayout_2.setObjectName("horizontalLayout_2") self.Current = QtWidgets.QLabel(parent=Form) font = QtGui.QFont() font.setFamily("Secular One") font.setPointSize(20) self.Current.setFont(font) self.Current.setObjectName("Current") self.horizontalLayout_2.addWidget(self.Current) self.Tasks = QtWidgets.QLabel(parent=Form) font = QtGui.QFont() font.setFamily("Secular One") font.setPointSize(20) self.Tasks.setFont(font) self.Tasks.setObjectName("Tasks") self.horizontalLayout_2.addWidget(self.Tasks) self.verticalLayout.addLayout(self.horizontalLayout_2) self.progressBar = QtWidgets.QProgressBar(parent=Form) font = QtGui.QFont() font.setFamily("Secular One") font.setPointSize(20) self.progressBar.setFont(font) self.progressBar.setMinimum(0) self.progressBar.setProperty("value", 0) self.progressBar.setObjectName("progressBar") self.verticalLayout.addWidget(self.progressBar) self.Error = QtWidgets.QLabel(parent=Form) font = QtGui.QFont() font.setFamily("Secular One") font.setPointSize(20) self.Error.setFont(font) self.Error.setStyleSheet("color: red;") self.Error.setText("") self.Error.setObjectName("Error") self.verticalLayout.addWidget(self.Error) self.retranslateUi(Form) QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): _translate = QtCore.QCoreApplication.translate Form.setWindowTitle(_translate("Form", "Form")) self.Estimation.setText(_translate("Form", "estimated time:")) self.Current.setText(_translate("Form", "current task:")) self.Tasks.setText(_translate("Form", "task x out of y"))
补充测试代码
class TestLoading(TestCase): def test_task(self): def foo(loading_page: Loading): time.sleep(5) app = QApplication([]) self.loader = Loading(None, None,"doing something", 2, foo, 2, "testing") self.loader.show() app.exec()
问题原因与解决方案
1. 核心问题分析
- 信号绑定错误:
thread_pool.destroyed信号是对象被销毁时才触发,而QThreadPool默认不会自动销毁,所以这个信号永远不会触发,quit()方法根本不会被调用。 - closeEvent死锁:
self.thread_pool.waitForDone(-1)会无限阻塞主线程等待线程池完成,但此时主线程负责UI渲染,阻塞后窗口无法响应关闭事件,形成死锁。 - 任务完成无关闭触发:当前
task_done仅更新UI,没有触发窗口关闭的逻辑。
2. PyQt6中QThreadPool的特性
PyQt6的QThreadPool是长驻型线程池,设计目标是复用线程、减少创建销毁开销,默认不会在任务完成后自动关闭或销毁。这和Rust中部分线程池的“无任务自动退出”机制不同——Qt线程池会保持线程存活,等待后续任务提交,直到被主动销毁或程序退出。
3. 修复方案
方案1:任务完成后主动触发关闭
修改task_done方法,当所有任务完成时直接调用窗口关闭:
def task_done(self, next_task: str = None): self.tasks_done += 1 progress = int((self.tasks_done / self.all_tasks) * 100) self.progressBar.setValue(progress) if not next_task: self.Current.setText("finished all tasks, closing window") self.Tasks.setText(f"{self.tasks_done} out of {self.all_tasks}") self.close() # 任务完成直接触发关闭 elif self.tasks_done != self.all_tasks: self.Current.setText(f"currently: {next_task}") self.Tasks.setText(f"{self.tasks_done} out of {self.all_tasks}")
方案2:优化closeEvent避免死锁
移除无限等待,设置超时并清除未启动任务:
def closeEvent(self, a0): self.timer.stop() self.thread_pool.clear() # 清除未启动的任务 self.thread_pool.waitForDone(3000) # 等待3秒后强制关闭 super().closeEvent(a0) # 调用父类方法确保正常关闭
方案3:任务完成后销毁线程池(可选)
如果需要彻底销毁线程池,可在任务完成时调用deleteLater():
def task_done(self, next_task: str = None): # ... 其他逻辑 if not next_task: # ... UI更新逻辑 self.thread_pool.deleteLater() # 标记线程池待销毁 self.close()
4. 完整修复后的Loading类代码
from src.gui.loading import Ui_Form from PyQt6.QtWidgets import QWidget from PyQt6.QtCore import QThreadPool, QRunnable, QTimer from typing import Callable class Loading(Ui_Form, QWidget): def __init__(self, parent: QWidget | None, next_widget: QWidget | None, action: str, time: int, task: Callable, task_len: int, initial_task: str): super().__init__() self.setupUi(self) self.setParent(parent) self.parent = parent self.next_widget = next_widget self.time = time self.task = TaskRunner(self, task) self.current_time = 0 self.tasks_done = 0 self.all_tasks = task_len # 修正之前清空Task文本的错误 self.Task.setText(action) self.Estimation.setText(f"estimated time: {self.int_to_time(time)}") self.progressBar.setValue(0) self.TimeLeft.setText("") self.Current.setText("") self.thread_pool = QThreadPool(self) self.run_tasks() self.task_done(initial_task) self.timer = QTimer(self) self.timer.timeout.connect(self.update_time) self.timer.start(1000) @staticmethod def int_to_time(time: int) -> str: if time >= 3600: return f
相关产品推荐
相关产品推荐

