我正在使用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__
中放置一个断点,执行将按预期停止。如果我恢复执行,它does会在我的Child.stop_thread
断点处停止。那么关于暂停析构函数的某些事情是允许信号被Child线程捕获?无论如何,如果没有断点,这将不起作用,因此不起作用。
从长远来看,我删除了所有这些并做了一些看起来很愚蠢的事情:
# 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
函数(在此玩具中和在我的实际代码中)都是隐式线程安全的。
因此,这引出我的问题:
经过更多的实验,如果您将连接设为Qt.BlockingQueuedConnection
,则似乎信号/插槽接近将起作用。
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()