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

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
相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 11:22:05