PyQt: простая работа с потоками

Очень часто в программах приходится использовать многопоточность. Иногда это монстрообразные пулы потоков со сложным взаимодействием, но гораздо чаще это бывает простой код, главное требование к которому — не замораживать интерфейс.

В PyQt есть два основных средства работы с потоками высокого уровня: питоновский threading и Qt'овский QThread. Для меня QThread оказался предпочтительней из-за лучшей связи с механизмом сигналов-слотов в Qt.

Итак, инструмент выбран, все отлично работает, но со временем захотелось сделать работу с потоками несколько проще и удобней. Возникла идея сделать велосипед модуль работы с потоками для простых случаев.

simple_thread

Этот модуль предназначен для работы с потоками в классах, унаследованных от QObject. С помощью него можно заставить любой метод класса выполняться в отдельном потоке, при этом изнутри метода можно обращаться (хотя и ограниченно) к атрибутам и методам класса.

Разберем на простом примере:
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys
from time import sleep
from PyQt4.QtCore import *
from PyQt4.QtGui import *

from simple_thread import SimpleThread


class Foo(QLabel):
    def __init__(self, parent = None):
        QLabel.__init__(self, parent)
        self.setFixedSize(320, 240)
        self.digits = ['one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten']

    @SimpleThread
    def bar(self, primaryText):
        rows = []
        digits = self.digits
        for item in digits:
            rows.append('%s: %s' % (primaryText, item))
            self.setText('\n'.join(rows), thr_method = 'b')
            sleep(0.5)
            
    def setText(self, text):
        QLabel.setText(self, text)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    foo = Foo()
    foo.show()
    foo.bar('From thread', thr_start = True)
    app.exec_()


Класс Foo наследуется от QLabel, и мы хотим раз в полсекунды менять текст метки, не замораживая интерфейс. Выводом текста у нас будет заниматься метод bar. Чтобы работа этого метода происходила в отдельном потоке, перед объявлением метода ставим декоратор @SimpleThread.

Изнутри метода нам будет необходим доступ к атрибуту digits — списку выводимых слов. Также нам необходимо обновлять текст метки, для этого мы будем вызывать метод setText.

Доступ к атрибутам

Первая проблема — digits может использоваться не только этим потоком.
Модуль simple_thread позволяет обойти эту проблему. Когда мы получаем атрибут, нам возвращается не ссылка на этот атрибут, а его копия. При этом, все действия происходят в контексте основного потока, и нам не надо беспокоиться об одновременном доступе к атрибуту из нескольких потоков.
Таким образом можно обращаться к спискам, словарям и всем неизменяемым атрибутам класса (строки, кортежи и т. п.).
Здесь есть один момент — это работает достаточно медленно, так что с доступом к атрибутам перебарщивать не стоит.

Вызов методов

Проблема номер два — вызов метода setText. Проблема похожа на первую — Qt выдаст исключение при попытке доступа к методу графического класса не из основного потока. Как и впервом случае, решается это приостановкой нашего потока и вызовом метода из основного потока.

Есть три различных варианта вызова методов, зависящих от аргумента thr_method:
  • thr_method = 'b': при вызове метода происходит приостановка нашего потока до окончания работы метода. Выполнение метода происходит в основном потоке. Только в этом варианте межно получить возвращаемое значение метода;
  • thr_method = 'q': в этом случае наш поток не останавливается. Метод так же выполняется в основном потоке;
  • thr_method = None или не задан: выполнение метода происходит в контексте нашего потока. Здесь надо быть внимательным — все ограничения, связанные с многопоточностью вступают в силу, в частности, в методе нельзя обращаться к атрибутам класса.

Аргумент thr_method не передается в вызываемый метод.

Установка атрибутов

Установка атрибутов из другого потока так же возможна.
self.newAttr = 'text'

Атрибуты могут быть любого типа.
Как и в остальных случаях, вся работа проводится в главном потоке и не вызывает проблем, стоит только помнить, что устанавливая атрибут, можно затереть одноименный существующий.

Запуск потока

Для запуска нашего кода мы просто выполняем метод bar, указав аргумент thr_start = True, чтобы поток стартовал немедленно.
Есть и другой способ, он пригодится, если мы захотим обрабатывать сигналы, вызываемые из другого потока или сигналы класса QThread (started, finished, terminated).
    thread = foo.bar('From thread')
    thread.finished.connect(self.barFinished)
    thread.start()

Здесь мы связали сигнал окончания работы потока finished с методом barFinished и запустили поток.

Остановка потока

Если по какой-то причине нам необходимо остановить выполняющийся поток, мы можем это сделать, вызвав метод thr_stop.
    thread = foo.bar('From thread', thr_start = True)
    ...
    thread.thr_stop()

Этот метод устанавливает флаг thr_stopFlag=True, состояние которого мы должны отслеживать в нашем методе и при его истинном значении заканчивать работу нашего метода.

Стоит отметить, что метод thr_stop не ждет остановки потока, возвращая управление сразу. Если же нам надо дождаться окончания работы потока, после thr_stop необходимо вызвать метод wait.

В модуле simple_thread есть две функции для остановки всех активных потоков — terminateThreads и closeThreads. Первая функция жестко прерывает выполнение всех потоков, что может быть небезопасно. Вторая же функция работает так, как если бы мы вызвали thr_stop для каждого потока, а затем wait.

Все замечания, предложения и тому подобное приветствуются. Если что-то подобное кем-то уже было реализовано, то буду рад ссылке.
Метки:
  • +22
  • 8,9k
  • 5
Поделиться публикацией
Похожие публикации
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 5
  • 0
    Первая проблема — digits может использоваться не только этим потоком. Модуль simple_thread позволяет обойти эту проблему. Когда мы получаем атрибут, нам возвращается не ссылка на этот атрибут, а его копия. При этом, все действия происходят в контексте основного потока, и нам не надо беспокоиться об одновременном доступе к атрибуту из нескольких потоков.

    В CPython такой проблемы нет — благодаря GIL в любой момент времени исполняеться только один поток. То есть можно не особо задумываясь обмениваться данными с стандартными python-объектами посредством атрибутов. Может возникнуть неожиданное поведение с Qt-объектами и это приводит нас к тому что:

    Такой код не совсем Qt-friendly. Намного более идиоматичный вариант — сделать свой поток с сигналом textChanged(str) и присоиденить его соответствующему слоту — setText с помощью Qt::QueuedConnection соединения. Кода это особо не прибавит, а читать это потом будет намного легче. Кроме того если возникнет желание можно будет переписать такие потоки на С++.
    • +1
      В CPython такой проблемы нет — благодаря GIL в любой момент времени исполняеться только один поток. То есть можно не особо задумываясь обмениваться данными с стандартными python-объектами посредством атрибутов.

      Да, непосредственно проблемы доступа к питоновским типам нет, но нельзя гарантировать атомарности группы операций. Здесь же мы не можем получить доступ к изменяемому объекту и изменить его — списки и словари копируются, а при попытке доступа к другим изменяемым объектам генерируется исключение.

      Такой код не совсем Qt-friendly. Намного более идиоматичный вариант — сделать свой поток с сигналом textChanged(str) и присоиденить его соответствующему слоту — setText с помощью Qt::QueuedConnection соединения. Кода это особо не прибавит, а читать это потом будет намного легче.

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

      Кроме того если возникнет желание можно будет переписать такие потоки на С++.

      Согласен, но у нас на работе гораздо чаще встречается обратная задача.
    • 0
      Получилась очень хрупкая концепция. Настолько, что аж непонятно — как этим пользоваться без опасения наступить на грабли.
      • +1
        Вообще, я старался уменьшить потенциальное количество грабель, но может сделал все наоборот. Если кому-то будет не лень покопаться в коде и найти грабли, заранее благодарю.

        Я сомневаюсь, что это имеет смысл использовать для действительно сложного многопоточного кода. Скорее этот модуль для простых случаев, когда нужно что-то выполнить в отдельном потоке, не мешая основному потоку.
      • 0
        О, спасибо, интересно. Сегодня опробую на практике.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.