ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 05.12.2023
Просмотров: 830
Скачиваний: 3
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
350
Часть II. Библиотека PyQt 5 def run(self): self.exec_() # Запускаем цикл обработки сигналов def on_start(self): self.count += 1 self.s1.emit(self.count) class Thread2(QtCore.QThread): s2 = QtCore.pyqtSignal(str) def __init__(self, parent=None):
QtCore.QThread.__init__(self, parent) def run(self): self.exec_() # Запускаем цикл обработки сигналов def on_change(self, i): i += 10 self.s2.emit("%d" % i) 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.button = QtWidgets.QPushButton("Сгенерировать сигнал") self.vbox = QtWidgets.QVBoxLayout() self.vbox.addWidget(self.label) self.vbox.addWidget(self.button) self.setLayout(self.vbox) self.thread1 = Thread1() self.thread2 = Thread2() self.thread1.start() self.thread2.start() self.button.clicked.connect(self.thread1.on_start) self.thread1.s1.connect(self.thread2.on_change) self.thread2.s2.connect(self.on_thread2_s2) def on_thread2_s2(self, s): self.label.setText(s) if __name__ == "__main__": import sys app = QtWidgets.QApplication(sys.argv) window = MyWindow() window.setWindowTitle("Обмен сигналами между потоками") window.resize(300, 70) window.show() sys.exit(app.exec_())
В этом примере мы создали классы
Thread1
,
Thread2
и
MyWindow
. Первые два класса пред- ставляют собой потоки. Внутри них в методе run()
вызывается метод exec_()
, который за- пускает цикл обработки событий. В конструкторе класса
MyWindow производится создание надписи, кнопки и экземпляров классов
Thread1
и
Thread2
. Далее выполняется запуск сразу двух потоков.
Глава 17. Знакомство с PyQt 5 351
В следующей инструкции сигнал нажатия кнопки соединяется с методом on_start()
перво- го потока. Внутри этого метода производится какая-либо операция (в нашем случае — уве- личение значения атрибута count
), а затем с помощью метода emit()
генерируется сигнал s1
, и в параметре передается результат выполнения метода. Сигнал s1
соединен с методом on_change()
второго потока. Внутри этого метода также производится какая-либо операция, а затем генерируется сигнал s2
, и передается результат выполнения метода. В свою очередь сигнал s2
соединен со слотом on_thread2_s2
объекта окна, который выводит в надпись зна- чение, переданное с этим сигналом. Таким образом, при нажатии кнопки Сгенерировать сигнал вначале будет вызван метод on_start()
из класса
Thread1
, затем метод on_change()
из класса
Thread2
, а потом метод on_thread2_s2
класса
MyWindow
, который выведет результат выполнения на экран.
17.9.3. Модуль queue: создание очереди заданий
В предыдущем разделе мы рассмотрели возможность обмена сигналами между потоками.
Теперь предположим, что запущены десять потоков, которые ожидают задания в бесконеч- ном цикле. Как передать задание одному потоку, а не всем сразу? И как определить, какому потоку передать задание? Можно, конечно, создать список в глобальном пространстве имен и добавлять задания в этот список, но в этом случае придется решать вопрос о совместном использовании одного ресурса сразу десятью потоками. Ведь если потоки будут получать задания одновременно, то одно задание могут получить сразу несколько потоков, и какому- либо потоку не хватит заданий, — возникнет исключительная ситуация. Попросту говоря, возникает ситуация, когда вы пытаетесь сесть на стул, а другой человек одновременно пы- тается вытащить его из-под вас. Думаете, что успеете сесть?
Модуль queue
, входящий в состав стандартной библиотеки Python, позволяет решить эту проблему. Модуль содержит несколько классов, которые реализуют разного рода потоко- безопасные очереди. Опишем эти классы:
Queue
— очередь (первым пришел, первым вышел). Формат конструктора:
<Объект> = Queue([maxsize=0])
Пример:
>>> import queue
>>> q = queue.Queue()
>>> q.put_nowait("elem1")
>>> q.put_nowait("elem2")
>>> q.get_nowait()
'elem1'
>>> q.get_nowait()
'elem2'
LifoQueue
— стек (последним пришел, первым вышел). Формат конструктора:
<Объект> = LifoQueue([maxsize=0])
Пример:
>>> q = queue.LifoQueue()
>>> q.put_nowait("elem1")
>>> q.put_nowait("elem2")
>>> q.get_nowait()
'elem2'
352
Часть II. Библиотека PyQt 5
>>> q.get_nowait()
'elem1'
PriorityQueue
— очередь с приоритетами. Элементы очереди должны быть кортежами, в которых первым элементом является число, означающее приоритет, а вторым — значе- ние элемента. При получении значения возвращается элемент с наивысшим приоритетом
(наименьшим значением в первом параметре кортежа). Формат конструктора класса:
<Объект> = PriorityQueue([maxsize=0])
Пример:
>>> q = queue.PriorityQueue()
>>> q.put_nowait((10, "elem1"))
>>> q.put_nowait((3, "elem2"))
>>> q.put_nowait((12, "elem3"))
>>> q.get_nowait()
(3, 'elem2')
>>> q.get_nowait()
(10, 'elem1')
>>> q.get_nowait()
(12, 'elem3')
Параметр maxsize во всех трех случаях задает максимальное количество элементов, которое может содержать очередь. Если параметр равен нулю (значение по умолчанию) или отрица- тельному значению, то размер очереди не ограничен.
Эти классы поддерживают следующие методы:
put(<Элемент>[, block=True][, timeout=None])
— добавляет элемент в очередь. Если в параметре block указано значение
True
, поток будет ожидать возможности добавления элемента, — при этом в параметре timeout можно указать максимальное время ожи- дания в секундах. Если элемент не удалось добавить, возбуждается исключение queue.Full
. В случае передачи параметром block значения
False очередь не будет ожи- дать, когда появится возможность добавить в нее новый элемент, и в случае невозмож- ности сделать это возбудит исключение queue.Full немедленно;
put_nowait(<Элемент>)
— добавление элемента без ожидания. Эквивалентно: put(<Элемент>, False)
get([block=True][, timeout=None])
— возвращает элемент, при этом удаляя его из оче- реди. Если в параметре block указано значение
True
, поток будет ожидать возможности извлечения элемента, — при этом в параметре timeout можно указать максимальное время ожидания в секундах. Если элемент не удалось получить, возбуждается исключе- ние queue.Empty
. В случае передачи параметром block значения
False очередь не будет ожидать, когда появится возможность извлечь из нее элемент, и в случае невозможности сделать это возбудит исключение queue.Empty немедленно;
get_nowait()
1 ... 29 30 31 32 33 34 35 36 ... 83
— извлечение элемента без ожидания. Эквивалентно вызову get(False)
;
join()
— блокирует поток, пока не будут обработаны все задания в очереди. Другие потоки после обработки текущего задания должны вызывать метод task_done()
. Как только все задания окажутся обработанными, поток будет разблокирован;
task_done()
— этот метод должны вызывать потоки после обработки задания;
qsize()
— возвращает приблизительное количество элементов в очереди. Так как дос- туп к очереди имеют сразу несколько потоков, доверять этому значению не следует — в любой момент времени количество элементов может измениться;
Глава 17. Знакомство с PyQt 5 353
empty()
— возвращает
True
, если очередь пуста, и
False
— в противном случае;
full()
— возвращает
True
, если очередь содержит элементы, и
False
— в противном случае.
Рассмотрим использование очереди в многопоточном приложении на примере (листинг 17.16).
Листинг 17.16. Использование модуля queue
# -*- coding: utf-8 -*- from PyQt5 import QtCore, QtWidgets import queue class MyThread(QtCore.QThread): task_done = QtCore.pyqtSignal(int, int, name = 'taskDone') def __init__(self, id, queue, parent=None):
QtCore.QThread.__init__(self, parent) self.id = id self.queue = queue def run(self): while True: task = self.queue.get() # Получаем задание self.sleep(5) # Имитируем обработку self.task_done.emit(task, self.id) # Передаем данные обратно self.queue.task_done() class MyWindow(QtWidgets.QPushButton): def __init__(self):
QtWidgets.QPushButton.__init__(self) self.setText("Раздать задания") self.queue = queue.Queue() # Создаем очередь self.threads = [] for i in range(1, 3): # Создаем потоки и запускаем thread = MyThread(i, self.queue) self.threads.append(thread) thread.task_done.connect(self.on_task_done, QtCore.Qt.QueuedConnection) thread.start() self.clicked.connect(self.on_add_task) def on_add_task(self): for i in range(0, 11): self.queue.put(i) # Добавляем задания в очередь def on_task_done(self, data, id): print(data, "- id =", id) # Выводим обработанные данные if __name__ == "__main__": import sys app = QtWidgets.QApplication(sys.argv) window = MyWindow() window.setWindowTitle("Использование модуля queue") window.resize(300, 30) window.show() sys.exit(app.exec_())
354
Часть II. Библиотека PyQt 5
В этом примере конструктор класса
MyThread принимает уникальный идентификатор (
id
) и ссылку на очередь (
queue
), которые сохраняются в одноименных атрибутах класса. В мето- де run()
внутри бесконечного цикла производится получение элемента из очереди с по- мощью метода get()
. Если очередь пуста, поток будет ожидать, пока не появится хотя бы один элемент. Далее производится обработка задания (в нашем случае — просто задержка), а затем обработанные данные передаются главному потоку через сигнал taskDone
, прини- мающий два целочисленных параметра. В следующей инструкции с помощью метода task_done()
указывается, что задание было обработано.
Отметим, что здесь в вызове функции pyqtSignal()
присутствует именованный параметр name
: task_done = QtCore.pyqtSignal(int, int, name = 'taskDone')
Он задает имя сигнала и может быть полезен в том случае, если это имя отличается от име- ни атрибута класса, соответствующего сигналу, — как в нашем случае, где имя сигнала taskDone отличается от имени атрибута task_done
. После чего мы можем обращаться к сигналу как по имени соответствующего ему атрибута: self.task_done.emit(task, self.id) так и по имени, заданному в параметре name функции pyqtSignal()
: self.taskDone.emit(task, self.id)
Главный поток реализуется с помощью класса
MyWindow
. Обратите внимание, что наследу- ется класс
QPushButton
(кнопка), а не класс
QWidget
. Все визуальные компоненты являются наследниками класса
QWidget
, поэтому любой компонент, не имеющий родителя, обладает своим собственным окном. В нашем случае используется только кнопка, поэтому можно сразу наследовать класс
QPushButton
Внутри конструктора класса
MyWindow с помощью метода setText()
задается текст надписи на кнопке, затем создается экземпляр класса
Queue и сохраняется в атрибуте queue
. В сле- дующем выражении производится создание списка, в котором будут храниться ссылки на объекты потоков. Сами объекты потоков (в нашем случае их два) создаются внутри цикла и добавляются в список. Внутри цикла производится также назначение обработчика сигнала taskDone и запуск потока с помощью метода start()
. Далее назначается обработчик нажа- тия кнопки.
При нажатии кнопки Раздать задания вызывается метод on_add_task()
, внутри которого производится добавление заданий в очередь. После этого потоки выходят из цикла ожи- дания, и каждый из них получает одно уникальное задание. После окончания обработки потоки генерируют сигнал taskDone и вызывают метод task_done()
, информирующий об окончании обработки задания. Главный поток получает сигнал и вызывает метод on_task_done()
, внутри которого через параметры будут доступны обработанные данные.
Так как метод расположен в GUI-потоке, мы можем изменять свойства компонентов и, на- пример, добавить результат в список или таблицу. В нашем же примере результат просто выводится в окно консоли (чтобы увидеть сообщения, следует сохранить файл с расшире- нием py
, а не pyw
). После окончания обработки задания потоки снова получают задания.
Если очередь окажется пуста, потоки перейдут в режим ожидания заданий.
17.9.4. Классы QMutex и QMutexLocker
Как вы уже знаете, совместное использование одного ресурса сразу несколькими потоками может привести к непредсказуемому поведению программы или даже аварийному ее
Глава 17. Знакомство с PyQt 5 355 завершению. То есть, доступ к ресурсу в один момент времени должен иметь лишь один поток. Следовательно, внутри программы необходимо предусмотреть возможность блоки- ровки ресурса одним потоком и ожидание его разблокировки другим потоком.
Реализовать блокировку ресурса в PyQt позволяют классы
QMutex и
QMutexLocker из модуля
QtCore
Конструктор класса
QMutex создает так называемый мьютекс и имеет следующий формат:
<Объект> = QMutex([mode=QtCore.QMutex.NonRecursive])
Необязательный параметр mode может принимать значения
NonRecursive
(поток может запросить блокировку только единожды, а после снятия блокировка может быть запрошена снова, — значение по умолчанию) и
Recursive
(поток может запросить блокировку не- сколько раз, и чтобы полностью снять блокировку, следует вызвать метод unlock()
соответ- ствующее количество раз).
Класс
QMutex поддерживает следующие методы:
lock()
— устанавливает блокировку. Если ресурс был заблокирован другим потоком, работа текущего потока приостанавливается до снятия блокировки;
tryLock([timeout=0])
— устанавливает блокировку. Если блокировка была успешно установлена, метод возвращает значение
True
, если ресурс заблокирован другим пото- ком — значение
False без ожидания возможности установить блокировку. Максималь- ное время ожидания в миллисекундах можно указать в качестве необязательного пара- метра timeout
. Если в параметре указано отрицательное значение, то метод tryLock()
ведет себя аналогично методу lock()
;
unlock()
— снимает блокировку;
isRecursive()
— возвращает
True
, если конструктору было передано значение
Recursive
Рассмотрим использование класса
QMutex на примере (листинг 17.17).
Листинг 17.17. Использование класса QMutex
# -*- coding: utf-8 -*- from PyQt5 import QtCore, QtWidgets class MyThread(QtCore.QThread): x = 10 # Атрибут класса mutex = QtCore.QMutex() # Мьютекс def __init__(self, id, parent=None):
QtCore.QThread.__init__(self, parent) self.id = id def run(self): self.change_x() def change_x(self):
MyThread.mutex.lock() # Блокируем print("x =", MyThread.x, "id =", self.id)
MyThread.x += 5 self.sleep(2) print("x =", MyThread.x, "id =", self.id)
356
Часть II. Библиотека PyQt 5
MyThread.x += 34 print("x =", MyThread.x, "id =", self.id)
MyThread.mutex.unlock() # Снимаем блокировку class MyWindow(QtWidgets.QPushButton): def __init__(self):
QtWidgets.QPushButton.__init__(self) self.setText("Запустить") self.thread1 = MyThread(1) self.thread2 = MyThread(2) self.clicked.connect(self.on_start) def on_start(self): if not self.thread1.isRunning(): self.thread1.start() if not self.thread2.isRunning(): self.thread2.start() if __name__ == "__main__": import sys app = QtWidgets.QApplication(sys.argv) window = MyWindow() window.setWindowTitle("Использование класса QMutex") window.resize(300, 30) window.show() sys.exit(app.exec_())
В этом примере в классе
MyThread мы создали атрибут x
, который доступен всем экземпля- рам класса. Изменение значения атрибута в одном потоке повлечет изменение значения и в другом потоке. Если потоки будут изменять значение одновременно, то предсказать текущее значение атрибута становится невозможным. Следовательно, изменять значение можно только после установки блокировки.
Чтобы обеспечить блокировку, внутри класса
MyThread создается экземпляр класса
QMutex и сохраняется в атрибуте mutex
. Обратите внимание, что сохранение производится в атрибуте объекта класса, а не в атрибуте экземпляра класса. Чтобы блокировка сработала, необходи- мо, чтобы защищаемый атрибут и мьютекс находились в одной области видимости. Далее весь код метода change_x()
, в котором производится изменение атрибута x
, помещается между вызовами методов lock()
и unlock()
мьютекса, — таким образом гарантируется, что он будет выполнен сначала одним потоком и только потом — другим.
Внутри конструктора класса
MyWindow производится создание двух экземпляров класса
MyThread и назначение обработчика нажатия кнопки. По нажатию кнопки Запустить будет вызван метод on_start()
, внутри которого производится запуск сразу двух потоков одно- временно, — при условии, что потоки не были запущены ранее. В результате мы получим в окне консоли следующий результат: x = 10 id = 1 x = 15 id = 1 x = 49 id = 1 x = 49 id = 2 x = 54 id = 2 x = 88 id = 2
Глава 17. Знакомство с PyQt 5 357
Как можно видеть, сначала изменение атрибута произвел поток с идентификатором
1
, а лишь затем — поток с идентификатором
2
. Если блокировку не указать, то результат будет иным: x = 10 id = 1 x = 15 id = 2 x = 20 id = 1 x = 54 id = 1 x = 54 id = 2 x = 88 id = 2
В этом случае поток с идентификатором
2
изменил значение атрибута x
до окончания вы- полнения метода change_x()
в потоке с идентификатором
1
При возникновении исключения внутри метода change_x()
ресурс останется заблокирован- ным, т. к. вызов метода unlock()
не будет выполнен. Кроме того, можно по случайности забыть вызвать метод unlock()
, что также приведет к вечной блокировке.
Исключить подобную ситуацию позволяет класс
QMutexLocker
. Конструктор этого класса принимает объект мьютекса и устанавливает блокировку. После выхода из области видимо- сти будет вызван деструктор класса, внутри которого блокировка автоматически снимется.
Следовательно, если создать экземпляр класса
QMutexLocker в начале метода, то после вы- хода из метода блокировка будет снята. Переделаем метод change_x()
из класса
MyThread и используем класс
QMutexLocker
(листинг 17.18).
Листинг 17.18. Использование класса QMutexLocker def change_x(self): ml = QtCore.QMutexLocker(MyThread.mutex) print("x =", MyThread.x, "id =", self.id)
MyThread.x += 5 self.sleep(2) print("x =", MyThread.x, "id =", self.id)
MyThread.x += 34 print("x =", MyThread.x, "id =", self.id)
# Блокировка автоматически снимется
При использовании класса
QMutexLocker следует помнить о разнице между областями видимости в языках C++ и Python. В языке C++ область видимости ограничена блоком, которым может являться как функция, так и просто область, ограниченная фигурными скобками. Таким образом, если переменная объявлена внутри блока условного оператора, например, if
, то при выходе из этого блока переменная уже не будет видна: if (условие) { int x = 10; // Объявляем переменную
// ...
}
// Здесь переменная x уже не видна!
В языке Python область видимости гораздо шире. Если мы объявим переменную внутри условного оператора, то она будет видна и после выхода из этого блока:
358
Часть II. Библиотека PyQt 5 if условие: x = 10 # Объявляем переменную
# ...
# Здесь переменная x еще видна
Таким образом, область видимости локальной переменной в языке Python ограничена функцией, а не любым блоком.
Класс
QMutexLocker поддерживает протокол менеджеров контекста, который позволяет ограничить область видимости блоком инструкции with...as
. Этот протокол гарантирует снятие блокировки, даже если внутри инструкции with...as будет возбуждено исключение.
Переделаем метод change_x()
из класса
MyThread снова и используем в этот раз инструкцию with...as
(листинг 17.19).
Листинг 17.19. Использование инструкции with...as def change_x(self): with QtCore.QMutexLocker(MyThread.mutex): print("x =", MyThread.x, "id =", self.id)
MyThread.x += 5 self.sleep(2) print("x =", MyThread.x, "id =", self.id)
MyThread.x += 34 print("x =", MyThread.x, "id =", self.id)
# Блокировка автоматически снимется
Теперь, когда вы уже знаете о возможности блокировки ресурса, следует сделать несколько замечаний:
установка и снятие блокировки занимают некоторый промежуток времени, тем самым снижая эффективность всей программы. Поэтому встроенные типы данных не обеспечи- вают безопасную работу в многопоточном приложении. И прежде чем использовать блокировки, подумайте — может быть, в вашем приложении они и не нужны;
второе замечание относится к доступу к защищенному ресурсу из GUI-потока. Ожида- ние снятия блокировки может заблокировать GUI-поток, и приложение перестанет реа- гировать на события. Поэтому в таком случае следует использовать сигналы, а не пря- мой доступ;
и последнее замечание относится к взаимной блокировке. Если первый поток, владея ресурсом
A
, захочет получить доступ к ресурсу
B
, а второй поток, владея ресурсом
B
, за- хочет получить доступ к ресурсу
A
, то оба потока будут ждать снятия блокировки вечно.
В этой ситуации следует предусмотреть возможность временного освобождения ресур- сов одним из потоков после превышения периода ожидания.
Класс
QMutexLocker также поддерживает методы unlock()
и relock()
. Первый метод вы- полняет разблокировку мьютекса без уничтожения экземпляра класса
QMutexLocker
, а вто- рой выполняет повторное наложение блокировки.
П
РИМЕЧАНИЕ
Для синхронизации и координации потоков предназначены также классы QSemaphore и
QWaitCondition
. За подробной информацией по этим классам обращайтесь к документа- ции по PyQt. Следует также помнить, что в стандартную библиотеку языка Python входят модули multiprocessing и threading, которые позволяют работать с потоками в любом приложении. Однако при использовании PyQt нужно отдать предпочтение классу QThread, т. к. он позволяет работать с сигналами.