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的主要内容,如果未能解决你的问题,请参考以下文章