ээдд. Прохоренок_Н_А__Дронов_В_А_Python_3_и_PyQt_5_Разработка_приложен. Николай Прохоренок Владимир Дронов
Скачать 7.92 Mb.
|
— извлечение элемента без ожидания. Эквивалентно вызову 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, т. к. он позволяет работать с сигналами. Глава 17. Знакомство с PyQt 5 359 17.10. Вывод заставки В больших приложениях загрузка начальных данных может занимать продолжительное время, в течение которого принято выводить окно-заставку, в котором отображается про- цесс загрузки. По окончании инициализации приложения окно-заставка скрывается и ото- бражается главное окно. Для вывода окна-заставки в PyQt предназначен класс QSplashScreen из модуля QtWidgets Конструктор класса имеет следующие форматы: <Объект> = QSplashScreen([<Изображение>][, flags=<Тип окна>]) <Объект> = QSplashScreen(<Родитель>[, <Изображение>][, flags=<Тип окна>]) Параметр <Родитель> позволяет указать ссылку на родительский компонент. В параметре <Изображение> указывается ссылка на изображение (экземпляр класса QPixmap , объявленно- го в модуле QtGui ), которое будет отображаться на заставке. Конструктору класса QPixmap можно передать путь к файлу с изображением. Параметр flags предназначен для указания типа окна — например, чтобы заставка отображалась поверх всех остальных окон, следует передать флаг WindowStaysOnTopHint Класс QSplashScreen поддерживает следующие методы: show() — отображает заставку; finish(<Ссылка на окно>) — закрывает заставку. В качестве параметра указывается ссылка на главное окно приложения; showMessage(<Сообщение>[, <Выравнивание>[, <Цвет>]]) — выводит сообщение. Во вто- ром параметре указывается местоположение надписи в окне. По умолчанию надпись выводится в левом верхнем углу окна. В качестве значения можно через оператор | ука- зать комбинацию следующих флагов: AlignTop (по верху), AlignCenter (по центру вер- тикали и горизонтали), AlignBottom (по низу), AlignHCenter (по центру горизонтали), AlignVCenter (по центру вертикали), AlignLeft (по левой стороне), AlignRight (по пра- вой стороне). В третьем параметре указывается цвет текста. В качестве значения можно указать атрибут из класса QtCore.Qt (например, black (по умолчанию), white и т. д.) или экземпляр класса QColor (например, QColor("red") , QColor("#ff0000") , QColor(255, 0, 0) и др.); clearMessage() — стирает надпись; setPixmap(<Изображение>) — позволяет изменить изображение в окне. В качестве пара- метра указывается экземпляр класса QPixmap ; pixmap() — возвращает изображение в виде экземпляра класса QPixmap Пример кода, выводящего заставку, показан в листинге 17.20. А на рис. 17.6 можно увидеть эту заставку воочию. Листинг 17.20. Вывод заставки # -*- coding: utf-8 -*- from PyQt5 import QtCore, QtGui, QtWidgets import time class MyWindow(QtWidgets.QPushButton): def __init__(self): |