PyQt:如何终止可重用的 QThread

PyQt: how to terminate reusable QThread

大多数关于 QThread 的指南似乎都着重于在其 QObject 完成后删除 QThread。我想保留 QThread 和 QObject 并在我再次需要它们时重用它们。这也意味着我在管理它们的生命周期时需要更加小心,因为 Qt-python 绑定和它们的内存管理很容易导致一些意外行为。

我正在寻找将 QThread 和 QObject 的生命周期绑定到 GUI(QMainWindow、QFRAM 等...)的最佳实践,这应该在其销毁期间执行适当的清理。我准备了一个最小的工作示例,但也许社区可以指出它的缺陷并对其进行改进。

import sys
from PyQt6 import QtCore, QtWidgets
import time

class MyWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("MyWindow")
        self.setAttribute(QtCore.Qt.WidgetAttribute.WA_DeleteOnClose) # this is OK
        
        self.button = QtWidgets.QPushButton("Start thread")
        self.setCentralWidget(self.button)
        
        self.worker = MyObject()
        self.thread = MyThread()
        self.worker.moveToThread(self.thread)
        self.thread.started.connect(self.worker.run)
        self.worker.finished.connect(self.thread.quit)
        # self.worker.finished.connect(self.worker.deleteLater) # for singleshot task
        # self.thread.finished.connect(self.thread.deleteLater) # for singleshot task

        # self.thread.start() # for singleshot task
        self.button.clicked.connect(self.thread.start)
        
    def __del__(self):
        print("MyWindow __del__ entered")
        self.thread.quit()
        print("MyWindow __del__ quit issued, waiting on thread")
        self.thread.wait()
        print("MyWindow __del__ thread returned")
        self.worker.deleteLater()
        self.thread.deleteLater()
        print("MyWindow __del__ quit")
        
    # def closeEvent(self, qCloseEvent):
    #     qCloseEvent.accept() # Also leads to crashes
    
class MyObject(QtCore.QObject):
    finished = QtCore.pyqtSignal()
    
    def __init__(self):
        super().__init__()
        
    def run(self):
        print("MyObject run entered")
        time.sleep(1)
        self.finished.emit()
        print("MyObject run quit")

class MyThread(QtCore.QThread):
    def __init__(self):
        super().__init__()
    
    def run(self):
        print("MyThread run entered")
        self.exec()
        print("MyThread run quit")
        
if __name__ == '__main__':
    app = QtCore.QCoreApplication.instance()
    if app is None:
        app = QtWidgets.QApplication(sys.argv)
    mainGui = MyWindow()
    mainGui.show()
    # app.aboutToQuit.connect(app.deleteLater) # Causes crashes
    app.exec()

此代码产生以下结果:

In [1]: runfile(...)
MyObject run entered
MyObject run quit
MyThread run entered
MyThread run quit
MyObject run entered
MyWindow __del__ entered
MyWindow __del__ quit issued, waiting on thread
MyWindow __del__ thread returned
MyWindow __del__ quit
MyObject run quit
MyThread run entered

In [2]: runfile(...)
MyThread run quit

这表示线程没有被正确删除。此外,令我惊讶的是 __del__ 函数在调用 thread.wait() 时没有被阻塞,因此线程在 QWidget 中幸存下来。不过至少多次调用不会崩溃

如果您要重新使用该线程,则没有必要进行任何显式清理,因为 Python/Qt 会在程序退出时自动处理。你应该做的是向你的工作人员 class 添加某种 stop() 方法,这样你就可以在 main-window:[=14 的关闭事件中优雅地终止线程=]

class MyWindow(QtWidgets.QMainWindow):
    ...

    def closeEvent(self, event):
        self.worker.stop()    
        self.thread.quit()
        self.thread.wait()

在您的示例中无需实施 __del__ 或调用 deleteLater()。更一般地说,显式清理通常只需要在正常运行时(即程序退出之前)删除的对象,以避免内存泄漏。

我正在回答我的问题,以提供一个示例应用程序,该示例应用程序激发了可重用线程体系结构,并且还迭代了@ekhumoro 的答案。在主线程上,您将消息排入队列并启动另一个线程来处理队列中的这些消息。您不必检查线程是否已经 运行 因为 QThread.start() 是幂等的。您始终保留同一线程的变量,以免引起垃圾收集崩溃。 定期检查 QThread.isInterruptionRequested() 有助于我们优雅地终止它。

import sys, collections
from PyQt6 import QtCore, QtWidgets

class LoggerWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Logger window")
        self.setAttribute(QtCore.Qt.WidgetAttribute.WA_DeleteOnClose)
        
        self.button = QtWidgets.QPushButton("Start thread")
        self.setCentralWidget(self.button)
        
        self.message_queue = collections.deque()
        
        self.worker = LoggerWindow.Worker(self.message_queue)
        self.thread = QtCore.QThread()
        self.worker.moveToThread(self.thread)
        self.thread.started.connect(self.worker.process_messages)
        
        self.button.clicked.connect(self.enqueue_message)
    
    @QtCore.pyqtSlot(bool)
    def enqueue_message(self, checked):
        self.message_queue.appendleft(len(self.message_queue) + 1)
        self.thread.start()
        
    def closeEvent(self, qCloseEvent):
        self.thread.requestInterruption()
        self.thread.quit()
        self.thread.wait()
        qCloseEvent.accept()
    
    class Worker(QtCore.QObject):
        def __init__(self, message_queue):
            super().__init__()
            self.message_queue = message_queue
            
        @QtCore.pyqtSlot()    
        def process_messages(self):
            while self.message_queue and not self.thread().isInterruptionRequested():
                self.thread().sleep(1)
                print(f"Processing message: {self.message_queue.pop()}/{len(self.message_queue)}")
            print("Finished processing!")
            self.thread().quit()
        
if __name__ == '__main__':
    app = QtCore.QCoreApplication.instance()
    if app is None:
        app = QtWidgets.QApplication(sys.argv)
    mainGui = LoggerWindow()
    mainGui.show()
    app.exec()