QThread не завершается

Я создаю графический интерфейс с подключением к сокету, работающим в фоновом потоке в PyQt5. Все работает довольно хорошо, за исключением того, что Qthread никогда не испускает готовый сигнал. Может быть, это не проблема, и я мог бы изменить свою реализацию, чтобы обойти это, но ожидаемое ли поведение, что Qthread продолжит работу после того, как перемещенный объект перестанет что-либо делать?

Должен ли я написать функцию в основном классе, чтобы остановить поток, как только я закончу с ним, или я могу просто перенести новые вещи в этот поток без каких-либо последствий?

class MyClass(PyQt5.QtWidgets.QMainWindow)
    def __init__(self, parent=None):

        # Setup thread for the socket control
        self.simulThread = PyQt5.QtCore.QThread()
        self.socketController = SocketController()
        self.socketController.moveToThread(self.simulThread)
        self.simulThread.started.connect(self.socketController.controlSocket)

        self.simulThread.finished.connect(self.deadThread)

        # Bind controls to events
        self.ui.buttonConnect.clicked.connect(self.simulThread.start)
        self.ui.buttonDisconnect.clicked.connect(lambda: self.socketController.stop())

   def deadThread(self, data):
       print ("THREAD IS DEAD.")

class SocketController(PyQt5.QtCore.QObject):
    finished       = PyQt5.QtCore.pyqtSignal()

    def __init__(self):
        super(SocketController, self).__init__()
        self.run = True;

    def controlSocket(self):
        #setup the socket

        while self.run:
            # do socket stuff
            time.sleep(1)

        #close the socket
        self.finished.emit()

    def stop(self):
        self.run = False;

person Ian    schedule 23.12.2015    source источник
comment
Вы имеете в виду, что THREAD IS DEAD печатается?   -  person Oliver    schedule 24.12.2015
comment
@Schollii не печатается.   -  person Ian    schedule 29.12.2015


Ответы (3)


QThread имеет собственный цикл событий для обработки сигналов. Метод controlSocket блокирует этот цикл событий до self.run == False. Но этого никогда не происходит, потому что self.run устанавливается в False только тогда, когда управление возвращается в цикл обработки событий, и он может обрабатывать сигнал, запускающий метод stop.

Вы, вероятно, захотите изменить архитектуру своего потока, чтобы вместо цикла while, который блокирует цикл событий QThread, вы создавали QTimer в потоке, который вызывает код #do socket stuff каждую 1 секунду. Таким образом, управление возвращается в цикл событий потоков, и он может обрабатывать стоп-сигнал (который будет изменен, чтобы предотвратить повторный запуск QTimer)

person three_pineapples    schedule 23.12.2015
comment
То, что вы говорите, имеет смысл, но эта строка: self.ui.buttonDisconnect.clicked.connect(lambda: self.socketController.stop()), кажется, на самом деле прерывает мой цикл while (поток выводит сообщение, которое приходит после цикла for, и сокет отключается). Итак, я считаю, что функция возвращается, но поток не завершается. - person Ian; 23.12.2015
comment
Также утверждается готовый сигнал, который я создал для класса SocketController, но не готовый сигнал из simulThread. - person Ian; 23.12.2015
comment
@Ian О, метод stop на самом деле запускается в основном потоке, потому что вы обернули соединение в лямбду (и поэтому магия Qt, которая определяет, в каком потоке что-то запускать, не работает, поскольку лямбда-выражения всегда существуют в потоке они были созданы в). Так что, думаю, у меня нет ответа... - person three_pineapples; 23.12.2015

На самом деле ответ представляет собой комбинацию того, что three_pineapples и OP опубликовали в своих ответах:

  1. Как отметил three_pineapples, основная проблема заключается в том, что controlSocket(), который вызывается циклом обработки событий потока, когда выдается сигнал запуска, не возвращается до тех пор, пока не будет вызван метод stop(). Однако в дизайне OP метод stop() может вызываться Qt только в том случае, если поток сокета может обрабатывать события (поскольку именно так отправляются сигналы между потоками через циклы событий). Это невозможно, пока controlSocket() занят зацикливанием и спящим режимом.
  2. Чтобы поток завершился, его цикл обработки событий должен быть остановлен.

Первую проблему необходимо устранить, либо разрешив потоку обрабатывать события во время цикла, либо используя таймер вместо цикла. Обработка событий показана в этом фрагменте кода на основе кода OP:

import time

from PyQt5.QtWidgets import QMainWindow, QWidget, QPushButton, QApplication, QHBoxLayout
from PyQt5.QtCore import QThread, QObject, pyqtSignal

class MyClass(QWidget):
    def __init__(self, parent=None):
        super().__init__()

        self.resize(250, 150)
        self.setWindowTitle('Simple')

        # Setup thread for the socket control
        self.socketController = SocketController()

        self.simulThread = QThread()
        self.socketController.moveToThread(self.simulThread)
        self.simulThread.started.connect(self.socketController.controlSocket)
        self.simulThread.finished.connect(self.deadThread)

        # Bind controls to events
        self.buttonConnect = QPushButton('connect')
        self.buttonConnect.clicked.connect(self.simulThread.start)

        self.buttonDisconnect = QPushButton('disconnect')
        self.buttonDisconnect.clicked.connect(self.socketController.stop)

        hbox = QHBoxLayout()
        hbox.addStretch(1)
        hbox.addWidget(self.buttonConnect)
        hbox.addWidget(self.buttonDisconnect)
        self.setLayout(hbox)

    def deadThread(self, data):
        print("THREAD IS DEAD.")


class SocketController(QObject):
    finished = pyqtSignal()

    def __init__(self):
        print('initialized')
        super().__init__()
        self.run = True

    def controlSocket(self):
        # setup the socket

        print('control socket starting')
        while self.run:
            # do socket stuff
            app.processEvents()
            print('control socket iterating')
            time.sleep(1)

        # close the socket
        self.finished.emit()
        print('control socket done')

    def stop(self):
        print('stop pending')
        self.run = False


app = QApplication([])
mw = MyClass()
mw.move(300, 300)
mw.show()
app.exec()

QTimer немного сложнее, но идея состоит в том, чтобы controlSocket() выполнял только одну итерацию цикла и многократно вызывал его через QTimer:

QTimer.singleShot(0, self.controlSocket)

а также

def controlSocket(self):
    # do socket stuff, then:
    if self.run:
        QTimer.singleShot(1000, self.controlSocket)
    else:
        self.finished.emit()

Любой из вышеперечисленных подходов устраняет первую проблему и позволяет SocketController прекратить выполнение своей работы, связанной с сокетом.

Вторая проблема заключается в том, что даже после того, как контроллер сокета выполнил свою работу, цикл потока все еще выполняется (эти два цикла независимы). Чтобы цикл обработки событий завершился, его необходимо остановить с помощью метода quit() потока. Уход и остановку следует выполнять вместе:

class MyClass(QWidget):
    def __init__(self, parent=None):
        ...

        self.buttonDisconnect = QPushButton('disconnect')
        self.buttonDisconnect.clicked.connect(self.stop)

        hbox = QHBoxLayout()
        hbox.addStretch(1)
        hbox.addWidget(self.buttonConnect)
        hbox.addWidget(self.buttonDisconnect)
        self.setLayout(hbox)

    def stop(self):
        self.simulThread.quit()
        self.socketController.stop()
person Oliver    schedule 05.01.2016

Итак, я нашел решение, но я действительно не знаю, правильно ли оно. Вместо того, чтобы сопоставлять кнопку отключения с функцией остановки сокета, я сопоставил ее с функцией, определенной в MyClass, которая вызывает новую написанную мной функцию под названием controlDisconnect, которая также явно сообщает потоку о выходе.

# The changed line
self.ui.buttonDisconnect.clicked.connect(self.controlDisconnect)

def controlDisconnect(self):
    self.socketController.stop()
    self.simulThread.quit()

Хотя это работает (поток выводит сообщение о том, что он умер), я не уверен, что это действительно хорошая практика. Вероятно, должен быть хотя бы какой-то код, чтобы убедиться, что socketController действительно остановился, прежде чем сообщить потоку о выходе.

person Ian    schedule 03.01.2016
comment
Что-то здесь не так. Вызывается ли метод controlSocket()? Поскольку у него есть бесконечный цикл, который останавливается только при вызове stop(), я ожидал, что этот метод никогда не вернется до остановки(), однако stop() может быть вызван только функцией controlDisconnect(), которая может быть вызвана только в том случае, если поток обрабатывает события, чего не может произойти, если он застрял в бесконечном цикле.... Примечание. В вашем ответе нет необходимости в лямбде (или в вашем вопросе, но тогда вам нужно удалить круглые скобки вызова ( )). - person Oliver; 03.01.2016
comment
@Schollii, спасибо за уловку лямбды, это артефакт того, что я все еще не совсем понимаю всю эту настройку потоков. В любом случае, да, controlSocket привязан к событию запуска потока. Возможно, это неясно из того, что я написал выше, но controlDisconnect находится в главном окне, а не в классе потока. - person Ian; 04.01.2016
comment
Может быть, я что-то неправильно понимаю, но controlDisconnect является частью MyClass и вызывается событием нажатия кнопки. Таким образом, его выполнение не зависит от потока. - person Ian; 04.01.2016