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()
大多数关于 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()