Шаг 15.
Библиотека PyQt5. Знакомство с PyQt5. Многопоточные приложения. Управление циклом внутри потока

    На этом шаге мы рассмотрим особенности такого управления.

    Очень часто внутри потока одни и те же инструкции выполняются многократно. Например, при осуществлении мониторинга серверов в Интернете на каждой итерации цикла посылается запрос к одному и тому же серверу. При этом внутри метода run() используется бесконечный цикл, выход из которого производится после окончания опроса всех серверов. В некоторых случаях этот цикл необходимо прервать преждевременно по нажатию кнопки пользователем. Чтобы это стало возможным, в классе, реализующем поток, следует создать атрибут, который будет содержать флаг текущего состояния. Далее на каждой итерации цикла мы проверяем состояние флага и при его изменении прерываем выполнение цикла. Чтобы изменить значение атрибута, создаем обработчик и связываем его с сигналом clicked() соответствующей кнопки. При нажатии кнопки внутри обработчика производим изменение значения атрибута. Пример запуска и остановки потока с помощью кнопок.

# -*- coding: utf-8 -*-
from PyQt5 import QtCore, QtWidgets

class MyThread(QtCore.QThread):
    mysignal = QtCore.pyqtSignal(str)
    def __init__(self, parent = None):
        QtCore.QThread.__init__(self, parent)
        self.running = False  # Флаг выполнения
        self.count = 0
    def run(self):
        self.running = True
        while self.running:   # Проверяем значение флага
            self.count += 1
            self.mysignal.emit("count = %s" % self.count)
            self.sleep(1)     # Имитируем процесс

class MyWindow(QtWidgets.QWidget):
    def __init__(self, parent = None):
        QtWidgets.QWidget.__init__(self, parent)
        self.label = QtWidgets.QLabel("Нажмите кнопку для запуска потока")
        self.label.setAlignment(QtCore.Qt.AlignHCenter)
        self.btnStart = QtWidgets.QPushButton("Запустить поток")
        self.btnStop = QtWidgets.QPushButton("Остановить поток")
        self.vbox = QtWidgets.QVBoxLayout()
        self.vbox.addWidget(self.label)
        self.vbox.addWidget(self.btnStart)
        self.vbox.addWidget(self.btnStop)
        self.setLayout(self.vbox)
        self.mythread = MyThread()
        self.btnStart.clicked.connect(self.on_start)
        self.btnStop.clicked.connect(self.on_stop)
        self.mythread.mysignal.connect(self.on_change, QtCore.Qt.QueuedConnection)
    def on_start(self):
        if not self.mythread.isRunning():
            self.mythread.start()     # Запускаем поток
    def on_stop(self):
        self.mythread.running = False # Изменяем флаг выполнения
    def on_change(self, s):
        self.label.setText(s)
    def closeEvent(self, event):      # Вызывается при закрытии окна
        self.hide()                   # Скрываем окно
        self.mythread.running = False # Изменяем флаг выполнения
        self.mythread.wait(5000)      # Даем время, чтобы закончить
        event.accept()	              # Закрываем окно

        
if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    window = MyWindow()
    window.setWindowTitle("Запуск и остановка потока")
    window.resize(300, 70)
    window.show()
    sys.exit(app.exec_())
Архив с файлом можно взять здесь.

    Результат работы приложения изображен на рисунке 1.


Рис.1. Результат работы приложения

    В этом примере внутри конструктора класса MyThread создается атрибут running, и ему присваивается значение False. При запуске потока внутри метода run() значение атрибута изменяется на True. Затем запускается цикл, в котором атрибут указывается в качестве условия. Как только значение атрибута станет равным значению False, цикл будет остановлен.

    Внутри конструктора класса MyWindow производится создание надписи, двух кнопок и экземпляра класса MyThread. Далее назначаются обработчики сигналов. При нажатии кнопки Запустить поток запустится метод on_start(), внутри которого с помощью метода isRunning() производится проверка текущего статуса потока. Если поток не запущен, выполняется его запуск вызовом метода start(). При нажатии кнопки Остановить поток запустится метод on_stop(), в котором атрибуту running присваивается значение False. Это значение является условием выхода из цикла внутри метода run().

    Путем изменения значения атрибута можно прервать выполнение цикла только в том случае, если закончилось выполнение очередной итерации. Если поток длительное время ожидает какого-либо события, - например, ответа сервера, то можно так и не дождаться завершения потока. Чтобы принудительно прервать выполнение потока, следует воспользоваться методом terminate(). Однако к этому методу следует прибегать только в крайнем случае, поскольку прерывание производится в любой части кода. При этом блокировки автоматически не снимаются, а кроме того, можно повредить данные, над которыми производились операции в момент прерывания. После вызова метода terminate() следует вызвать метод wait().

    При закрытии окна приложение завершает работу, что также приводит к завершению всех потоков. Чтобы предотвратить повреждение данных, мы перехватываем событие закрытия окна и дожидаемся окончания выполнения. Чтобы перехватить событие, необходимо внутри класса создать метод с предопределенным названием, в нашем случае - с названием closeEvent(). Этот метод будет автоматически вызван при попытке закрыть окно. В качестве параметра метод принимает объект события event, через который можно получить дополнительную информацию о событии. Чтобы закрыть окно внутри метода closeEvent(), следует вызвать метод accept() объекта события. Если необходимо предотвратить закрытие окна, то следует вызвать метод ignore().

    Внутри метода closeEvent() мы присваиваем атрибуту running значение False. Далее с помощью метода wait() даем возможность потоку нормально завершить работу. В качестве параметра метод wait() принимает количество миллисекунд, по истечении которых управление будет передано следующей инструкции. Необходимо учитывать, что это максимальное время: если поток закончит работу раньше, то и метод закончит выполнение раньше. Метод wait() возвращает значение True, если поток успешно завершил работу, и False - в противном случае. Ожидание завершения потока занимает некоторое время, в течение которого окно будет по-прежнему видимым. Чтобы не вводить пользователя в заблуждение, в самом начале метода closeEvent() мы скрываем окно вызовом метода hide().

    На следующем шаге мы закончим изучение этого вопроса.




Предыдущий шаг Содержание Следующий шаг