PyQt:如何终止可重用的 QThread

Posted

技术标签:

【中文标题】PyQt:如何终止可重用的 QThread【英文标题】:PyQt: how to terminate reusable QThread 【发布时间】:2021-10-25 13:06:29 【问题描述】:

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

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

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 中幸存下来。但至少它不会在多次调用后崩溃。

【问题讨论】:

【参考方案1】:

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

class MyWindow(QtWidgets.QMainWindow):
    ...

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

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

【讨论】:

感谢您的回答和编辑!您究竟会在worker.stop() 中添加什么内容?我可以考虑给self.thread().requestInterruption() 打电话? 不——你不能中断线程,因为它被工作线程阻塞了。您必须直接打断工人本身。对于带有循环的顺序处理,您可以使用一个简单的标志(例如while self._running: ...),该标志可以通过 stop 方法进行切换。但是,如果任务受 CPU 限制并且不能被分解成块,那么实际上没有办法 来停止它,因为 python 不支持真正的并发(由于 GIL)。对于这样的情况,多线程是错误的方法,您必须改用多处理。 PS:有一个工作示例here 应该让事情更清楚。它使用互斥锁来保护标志,但如果只有一个线程正在读取/写入它,这并不是绝对必要的 - 一个裸属性也可以很好地工作(如 this example 所示)。【参考方案2】:

我正在回答我的问题,以提供一个示例应用程序,该示例应用程序激发了可重用的线程架构,并在 @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()

【讨论】:

以上是关于PyQt:如何终止可重用的 QThread的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Django 中重用可重用的应用程序

PyQt5 QThread 不会因终止或标志变量而停止

如何编写可重用的 Javascript?

如何制作可用于可查询的可重用条件? [复制]

如何使我的代码成为可重用的组件?

如何在 Jetpack compose 中制作可重用的组件?