Я использую 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
. Итак, что-то в паузе в деструкторе - позволить сигналу попасть в поток 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
(как в этой игрушке, так и в моем реальном коде) неявно поточно-ориентирована.
Итак, это подводит меня к моим вопросам:
- Как правильно очистить потоки внуков, когда основной поток завершает работу?
- Что если, как в этом примере, я хочу, чтобы эти потоки внуков абстрагировались за объектом-членом?
- Разве подход с использованием сигналов / слотов не сработал?