PySide2:清理孙子线程的正确方法
PySide2: Proper way to clean up grandchild threads
我正在使用 PySide2 编写多线程应用程序。我在自己的线程中已经 运行 有一个 Controller 对象,这样它就不会阻塞 GUI 线程。该控制器需要启动多个连续执行工作的工作线程。我可以使用信号正确地手动启动和停止这些孙子线程,但是我在应用程序退出时使用信号清理它们时遇到了问题。
这是一个复制我的问题的玩具示例:
import sys
import shiboken2
from PySide2.QtCore import QObject, QThread
from PySide2.QtWidgets import QApplication, QPushButton
class Grandchild(QObject):
def __init__(self, parent=None):
super(Grandchild, self).__init__(parent)
print('Grandchild()')
def __del__(self):
print('~Grandchild()')
class Child(QObject):
_thread = None
_worker = None
def __init__(self, parent=None):
super(Child, self).__init__(parent)
print('Child()')
def __del__(self):
print('~Child()')
if shiboken2.isValid(self._thread):
self.stop_thread()
def start_thread(self):
print('Starting grandchild thread')
self._thread = QThread(self)
self._worker = Grandchild()
self._worker.moveToThread(self._thread)
self._thread.finished.connect(self._worker.deleteLater)
self._thread.start()
def stop_thread(self):
print('Stopping grandchild thread')
self._thread.quit()
self._thread.wait()
def toggle_thread(self):
if self._thread and self._thread.isRunning():
self.stop_thread()
else:
self.start_thread()
class Parent(QPushButton):
_thread = None
_worker = None
def __init__(self, parent=None):
super(Parent, self).__init__(parent)
print('Parent()')
self.setText('Start Grandchild')
self._thread = QThread(self)
self._worker = Child()
self._worker.moveToThread(self._thread)
self._thread.finished.connect(self._worker.deleteLater)
self._thread.start()
self.clicked.connect(self.on_push)
self.clicked.connect(self._worker.toggle_thread)
def __del__(self):
print('~Parent()')
if shiboken2.isValid(self._thread):
self._thread.quit()
self._thread.wait()
def on_push(self):
if self.text() == 'Start Grandchild':
self.setText('Stop Grandchild')
else:
self.setText('Start Grandchild')
def main():
app = QApplication(sys.argv)
widget = Parent()
widget.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
在此示例中,使用按钮启动和停止线程工作正常。如果我用按钮启动和停止孙子,然后关闭应用程序,所有析构函数都会正确调用并且应用程序会正确退出。如果我只启动孙子并关闭应用程序,我会得到:
Parent()
Child()
Starting grandchild thread
Grandchild()
QThread: Destroyed while thread is still running
~Parent()
Process finished with exit code 134 (interrupted by signal 6: SIGABRT)
我的第一个想法是像这样修改 Parent:
stop_child = Signal()
def __init__(self):
self.stop_child.connect(self._worker.stop_thread)
def __del__(self):
print('~Parent()')
self.stop_child.emit()
if shiboken2.isValid(self._thread):
self._thread.quit()
self._thread.wait()
照原样,这根本不影响问题。如果我在 Child.stop_thread
中放置一个断点,我可以看到在 SIGABRT 被抛出之前它永远不会被执行。如果我在 Parent.__del__
内放置一个断点,执行将按预期停止。如果我继续执行,它 会 在我的 Child.stop_thread
断点处停止。那么在析构函数中暂停是否允许信号在子线程中被捕获?无论如何,没有断点这就不起作用,所以那是行不通的。
我删除了所有这些并做了一些看起来非常愚蠢的事情(从长远来看):
# Parent
def __del__(self):
print('~Parent()')
self._worker.stop_thread() # Call the instance fn directly
if shiboken2.isValid(self._thread):
self._thread.quit()
self._thread.wait()
当然,它有效:
Parent()
Child()
Starting grandchild thread
Grandchild()
~Parent()
Stopping grandchild thread
~Child()
~Grandchild()
Process finished with exit code 0
为另一个线程中的对象调用实例函数似乎是一个非常糟糕的想法。我假设它可以工作,因为我的 stop_child
函数(在这个玩具和我的实际代码中)是隐式线程安全的。
所以这引出了我的问题:
- 当主线程关闭时清理孙线程的正确方法是什么?
- 如果像本例一样,我希望在成员对象后面抽象出那些孙子线程,该怎么办?
- signals/slots 方法不应该奏效吗?
经过更多实验后,如果您将连接设为 Qt.BlockingQueuedConnection
,signals/slots 方法 似乎 会起作用。
self.stop_child.connect(self._worker.stop_thread, Qt.BlockingQueuedConnection)
这意味着主线程阻塞,直到信号被传递,如果在正常运行时操作期间使用 stop_child
信号,这可能是不可取的。不过,在主小部件的析构函数中似乎没问题。
完整的工作示例如下:
import sys
import shiboken2
from PySide2.QtCore import QObject, QThread, Signal, Qt
from PySide2.QtWidgets import QApplication, QPushButton
class Grandchild(QObject):
def __init__(self, parent=None):
super(Grandchild, self).__init__(parent)
print('Grandchild()')
def __del__(self):
print('~Grandchild()')
class Child(QObject):
_thread = None
_worker = None
def __init__(self, parent=None):
super(Child, self).__init__(parent)
print('Child()')
def __del__(self):
print('~Child()')
if shiboken2.isValid(self._thread):
self.stop_thread()
def start_thread(self):
print('Starting grandchild thread')
self._thread = QThread(self)
self._worker = Grandchild()
self._worker.moveToThread(self._thread)
self._thread.finished.connect(self._worker.deleteLater)
self._thread.start()
def stop_thread(self):
print('Stopping grandchild thread')
self._thread.quit()
self._thread.wait()
def toggle_thread(self):
if self._thread and self._thread.isRunning():
self.stop_thread()
else:
self.start_thread()
class Parent(QPushButton):
_thread = None
_worker = None
stop_child = Signal()
def __init__(self, parent=None):
super(Parent, self).__init__(parent)
print('Parent()')
self.setText('Start Grandchild')
self._thread = QThread(self)
self._worker = Child()
self._worker.moveToThread(self._thread)
self._thread.finished.connect(self._worker.deleteLater)
self._thread.start()
self.clicked.connect(self.on_push)
self.clicked.connect(self._worker.toggle_thread)
self.stop_child.connect(self._worker.stop_thread, Qt.BlockingQueuedConnection)
def __del__(self):
print('~Parent()')
self.stop_child.emit()
if shiboken2.isValid(self._thread):
self._thread.quit()
self._thread.wait()
def on_push(self):
if self.text() == 'Start Grandchild':
self.setText('Stop Grandchild')
else:
self.setText('Start Grandchild')
def main():
app = QApplication(sys.argv)
widget = Parent()
widget.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
我正在使用 PySide2 编写多线程应用程序。我在自己的线程中已经 运行 有一个 Controller 对象,这样它就不会阻塞 GUI 线程。该控制器需要启动多个连续执行工作的工作线程。我可以使用信号正确地手动启动和停止这些孙子线程,但是我在应用程序退出时使用信号清理它们时遇到了问题。
这是一个复制我的问题的玩具示例:
import sys
import shiboken2
from PySide2.QtCore import QObject, QThread
from PySide2.QtWidgets import QApplication, QPushButton
class Grandchild(QObject):
def __init__(self, parent=None):
super(Grandchild, self).__init__(parent)
print('Grandchild()')
def __del__(self):
print('~Grandchild()')
class Child(QObject):
_thread = None
_worker = None
def __init__(self, parent=None):
super(Child, self).__init__(parent)
print('Child()')
def __del__(self):
print('~Child()')
if shiboken2.isValid(self._thread):
self.stop_thread()
def start_thread(self):
print('Starting grandchild thread')
self._thread = QThread(self)
self._worker = Grandchild()
self._worker.moveToThread(self._thread)
self._thread.finished.connect(self._worker.deleteLater)
self._thread.start()
def stop_thread(self):
print('Stopping grandchild thread')
self._thread.quit()
self._thread.wait()
def toggle_thread(self):
if self._thread and self._thread.isRunning():
self.stop_thread()
else:
self.start_thread()
class Parent(QPushButton):
_thread = None
_worker = None
def __init__(self, parent=None):
super(Parent, self).__init__(parent)
print('Parent()')
self.setText('Start Grandchild')
self._thread = QThread(self)
self._worker = Child()
self._worker.moveToThread(self._thread)
self._thread.finished.connect(self._worker.deleteLater)
self._thread.start()
self.clicked.connect(self.on_push)
self.clicked.connect(self._worker.toggle_thread)
def __del__(self):
print('~Parent()')
if shiboken2.isValid(self._thread):
self._thread.quit()
self._thread.wait()
def on_push(self):
if self.text() == 'Start Grandchild':
self.setText('Stop Grandchild')
else:
self.setText('Start Grandchild')
def main():
app = QApplication(sys.argv)
widget = Parent()
widget.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
在此示例中,使用按钮启动和停止线程工作正常。如果我用按钮启动和停止孙子,然后关闭应用程序,所有析构函数都会正确调用并且应用程序会正确退出。如果我只启动孙子并关闭应用程序,我会得到:
Parent()
Child()
Starting grandchild thread
Grandchild()
QThread: Destroyed while thread is still running
~Parent()
Process finished with exit code 134 (interrupted by signal 6: SIGABRT)
我的第一个想法是像这样修改 Parent:
stop_child = Signal()
def __init__(self):
self.stop_child.connect(self._worker.stop_thread)
def __del__(self):
print('~Parent()')
self.stop_child.emit()
if shiboken2.isValid(self._thread):
self._thread.quit()
self._thread.wait()
照原样,这根本不影响问题。如果我在 Child.stop_thread
中放置一个断点,我可以看到在 SIGABRT 被抛出之前它永远不会被执行。如果我在 Parent.__del__
内放置一个断点,执行将按预期停止。如果我继续执行,它 会 在我的 Child.stop_thread
断点处停止。那么在析构函数中暂停是否允许信号在子线程中被捕获?无论如何,没有断点这就不起作用,所以那是行不通的。
我删除了所有这些并做了一些看起来非常愚蠢的事情(从长远来看):
# Parent
def __del__(self):
print('~Parent()')
self._worker.stop_thread() # Call the instance fn directly
if shiboken2.isValid(self._thread):
self._thread.quit()
self._thread.wait()
当然,它有效:
Parent()
Child()
Starting grandchild thread
Grandchild()
~Parent()
Stopping grandchild thread
~Child()
~Grandchild()
Process finished with exit code 0
为另一个线程中的对象调用实例函数似乎是一个非常糟糕的想法。我假设它可以工作,因为我的 stop_child
函数(在这个玩具和我的实际代码中)是隐式线程安全的。
所以这引出了我的问题:
- 当主线程关闭时清理孙线程的正确方法是什么?
- 如果像本例一样,我希望在成员对象后面抽象出那些孙子线程,该怎么办?
- signals/slots 方法不应该奏效吗?
经过更多实验后,如果您将连接设为 Qt.BlockingQueuedConnection
,signals/slots 方法 似乎 会起作用。
self.stop_child.connect(self._worker.stop_thread, Qt.BlockingQueuedConnection)
这意味着主线程阻塞,直到信号被传递,如果在正常运行时操作期间使用 stop_child
信号,这可能是不可取的。不过,在主小部件的析构函数中似乎没问题。
完整的工作示例如下:
import sys
import shiboken2
from PySide2.QtCore import QObject, QThread, Signal, Qt
from PySide2.QtWidgets import QApplication, QPushButton
class Grandchild(QObject):
def __init__(self, parent=None):
super(Grandchild, self).__init__(parent)
print('Grandchild()')
def __del__(self):
print('~Grandchild()')
class Child(QObject):
_thread = None
_worker = None
def __init__(self, parent=None):
super(Child, self).__init__(parent)
print('Child()')
def __del__(self):
print('~Child()')
if shiboken2.isValid(self._thread):
self.stop_thread()
def start_thread(self):
print('Starting grandchild thread')
self._thread = QThread(self)
self._worker = Grandchild()
self._worker.moveToThread(self._thread)
self._thread.finished.connect(self._worker.deleteLater)
self._thread.start()
def stop_thread(self):
print('Stopping grandchild thread')
self._thread.quit()
self._thread.wait()
def toggle_thread(self):
if self._thread and self._thread.isRunning():
self.stop_thread()
else:
self.start_thread()
class Parent(QPushButton):
_thread = None
_worker = None
stop_child = Signal()
def __init__(self, parent=None):
super(Parent, self).__init__(parent)
print('Parent()')
self.setText('Start Grandchild')
self._thread = QThread(self)
self._worker = Child()
self._worker.moveToThread(self._thread)
self._thread.finished.connect(self._worker.deleteLater)
self._thread.start()
self.clicked.connect(self.on_push)
self.clicked.connect(self._worker.toggle_thread)
self.stop_child.connect(self._worker.stop_thread, Qt.BlockingQueuedConnection)
def __del__(self):
print('~Parent()')
self.stop_child.emit()
if shiboken2.isValid(self._thread):
self._thread.quit()
self._thread.wait()
def on_push(self):
if self.text() == 'Start Grandchild':
self.setText('Stop Grandchild')
else:
self.setText('Start Grandchild')
def main():
app = QApplication(sys.argv)
widget = Parent()
widget.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()