ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 12.12.2023
Просмотров: 97
Скачиваний: 3
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
Глава 4
Многопоточное программирование
194
операций проверки остальных, освобожденных блокировок произойдет мгновенно, без пауз. Наконец, необходимо полностью гарантировать, чтобы в заключительной паре строк выполнение функции main() происходило лишь при непосредственном вызове этого сценария.
Как было указано в предыдущем основном примечании, в данной главе модуль thread представлен исключительно для того, чтобы ознакомить читателя с началь- ными сведениями о многопоточном программировании. В многопоточном приложе- нии, как правило, следует использовать высокоуровневые модули, такие как модуль threading, который рассматривается в следующем разделе.
4.5. Модуль
threading
Перейдем к описанию высокоуровневого модуля threading, который не только предоставляет класс Thread, но и дает возможность воспользоваться широким раз- нообразием механизмов синхронизации, позволяющих успешно решать многие важные задачи. В табл. 4.2 представлен список всех объектов, имеющихся в модуле threading.
Таблица 4.2. Объекты модуля
threading
Объект
Описание
Thread
Объект, который представляет отдельный поток выполнения
Lock
Примитивный объект блокировки (такая же блокировка, как и в модуле thread)
Rlock
Реентерабельный объект блокировки предоставляет возможность в отдельном потоке (повторно) захватывать уже захваченную блокировку (это рекурсивная блокировка)
Condition
Объект условной переменной вынуждает один поток ожидать, пока определен- ное условие не будет выполнено другим потоком. Таким условием может быть изменение состояния или задание определенного значения данных
Event
Обобщенная версия условных переменных, которая позволяет обеспечить ожидание некоторого события любым количеством потоков, так что после об- наружения этого события происходит активизация всех потоков
Semaphore
Предоставляет счетчик конечных ресурсов, совместно используемый потоками; если ни один из ресурсов не доступен, происходит блокировка
BoundedSemaphore Аналогично Semaphore, но гарантируется, что превышение начального значе- ния никогда не произойдет
Timer
Аналогично
Thread, за исключением того, что происходит ожидание в течение выделенного промежутка времени перед выполнением
Barrier a
Создает барьер, которого должно достичь определенное количество потоков, прежде чем всем этим потокам будет разрешено продолжить работу a
Новое в Python 3.2.
В этом разделе описано, как использовать класс Thread для реализации многопо- точного режима работы. Выше в данной главе уже были приведены основные сведе- ния о блокировках, поэтому в данном разделе не будут рассматриваться примитивы
06_ch04.indd 194 22.01.2015 22:00:40
195 4.5. Модуль threading блокировки. Кроме того, сам класс Thread() позволяет решать некоторые задачи синхронизации, поэтому нет необходимости явно использовать примитивы блоки- ровки.
1 2 3 4 5 6 7 8
Потоки, функционирующие в качестве демонов
Еще одна причина, по которой следует избегать использование модуля thread, состо- ит в том, что он не поддерживает принцип организации работы программы на основе демонов (потоков, работающих в фоновом режиме). Модуль thread действует так, что после выхода из основного потока все дочерние потоки уничтожаются, без учета того, должны ли они продолжить выполнение определенной работы. Если это нежелательно, то можно организовать функционирование потоков в качестве демонов.
Поддержка демонов предусмотрена в модуле threading. Ниже описано, как они функ- ционируют. Обычно демон применяется в качестве сервера, который ожидает поступле- ния клиентских запросов, подлежащих выполнению. Если нет никакой работы, посту- пившей от клиентов, которая должна быть сделана, то демон простаивает. Для потока может быть установлен флаг, указывающий, что этот поток может выполнять роль демо- на. Такое указание равносильно обозначению родительского потока как не требующего после своего завершения, чтобы был завершен дочерний поток. Как было описано в гла- ве 2, потоки сервера функционируют в виде бесконечных циклов и в обычных ситуациях не завершают свою работу.
Дочерние потоки могут быть также обозначены флагами как демоны, если в основном потоке могут складываться условия готовности к выходу, но нет необходимости ожидать завершения работы дочерних потоков, чтобы выйти из основной программы. Значение true указывает, что дочерний поток не приносит результатов, от которых зависит воз- можность завершения всей программы, и в основном рассматривается как указание, что единственным назначением потока является ожидание запросов от клиентов и их обслу- живание.
Чтобы обозначить поток как выполняющий функции демона, необходимо применить оператор присваивания thread.daemon = True, прежде чем запустить этот поток.
(Устаревший способ осуществления этой задачи, заключавшийся в вызове операто- ра thread.setDaemon(True), теперь запрещен.) То же является справедливым в от- ношении проверки того, выполняет ли поток функции демона; достаточно проверить значение соответствующей переменной (а не вызывать функцию thread.isDaemon()).
Новый дочерний поток наследует свой флаг, обозначающий его в качестве демона, от родительского потока. Вся программа Python (рассматриваемая как основной поток) продолжает функционировать до тех пор, пока не произойдет выход из всех потоков, не обозначенных как демоны, иными словами, до тех пор, пока остаются активными какие- либо потоки, не действующие как демоны.
4.5.1. Класс
Thread
В программе с многопоточной организацией главным инструментом является класс Thread модуля threading. Модуль threading поддерживает целый ряд функ- ций, отсутствующих в модуле thread. В табл. 4.3 представлен список атрибутов и ме- тодов модуля threading.
06_ch04.indd 195 22.01.2015 22:00:40
Глава 4
Многопоточное программирование
196
Таблица 4.3. Атрибуты и методы объекта класса Thread
Атрибут
Описание
Атрибуты данных объекта потока
name
Имя потока
Ident
Идентификатор потока
Daemon
Булев флаг, указывающий, выполняет ли поток функции демона
Методы объекта потока
__init__(group=None,
target=None, name=None,
args=(), kwargs={},
verbose=None,daemon=None
)
c
Порождение объекта Thread с использованием целевого параме- тра
callable и набора параметров args или kwargs. Может быть также передан параметр
name или group, но обработка по- следнего не реализована. Принимается также флаг
verbose. Лю- бое ненулевое значение
daemon задает атрибут/флаг thread.
daemon
start()
Запуск выполнения потока run()
Метод, определяющий функционирование потока (обычно пере- крывается разработчиком приложения в подклассе)
join(timeout=None)
Приостановка до завершения запущенного потока; блокировка, если не задан параметр
timeout (в секундах)
getName()
a
Возвращаемое имя потока setName(name)
a
Заданное имя потока isAlive/is_alive()
b
Булев флаг, указывающий, продолжает ли поток работать isDaemon()
c
Возвращает
True, если поток выполняет функции демона, в про- тивном случае возвращает
False setDaemon(daemonic)
c
Задание флага работы в режиме демона равным указанному бу- леву значению
daemonic (вызов должен осуществляться перед выполнением функции start() для потока)
a
Обозначается как устаревший путем задания (или получения) атрибута thread.name или передачи его во время порождения экземпляра.
b
Имена в так называемом ВерблюжьемСтиле (CamelCase) рассматриваются как устарев- шие и заменяются, начиная с версии Python 2.6.
c
Метод is/setDaemon() обозначается как устаревший путем задания атрибута thread.
daemon; значение thread.daemon может быть также задано во время порождения экзем- пляра путем указания необязательного значения для демона; новое в версии Python 3.3.
Предусмотрен целый ряд способов, с помощью которых могут создаваться потоки на основе класса Thread. В данной главе рассматриваются три из этих способов, ко- торые мало отличаются друг от друга. Программист может выбрать способ, который является для него наиболее удобным, не говоря уже о том, что выбранный способ должен быть наиболее подходящим с точки зрения приложения и масштабирования в будущем (предпочтительным является последний из приведенных способов).
•
Создание экземпляра Thread с передачей функции.
•
Создание экземпляра Thread и передача вызываемого экземпляра класса.
•
Формирование подкласса Thread и создание экземпляра подкласса.
06_ch04.indd 196 22.01.2015 22:00:41
197 4.5. Модуль threading
Как правило, программисты выбирают первый или третий вариант. Последний становится предпочтительным, если требуется создать в большей степени объек- тно-ориентированный интерфейс, а первый — в противном случае. Второй вариант, откровенно говоря, является немного более громоздким, и, как показывает практика, его применение приводит к созданию программ, более сложных для восприятия.
Создание экземпляра
Thread с передачей функции
В первом примере будет лишь создан экземпляр Thread с передачей функции
(с ее параметрами) в форме, аналогичной предыдущим примерам. Именно эта функ- ция должна быть выполнена после передачи потоку указания, что он должен начать выполнение. Взяв за основу сценарий mtsleepB.py из примера 4.3 и внеся в него кор- ректировки, необходимые для использования объектов Thread, получим сценарий mtsleepC.py, как показано в примере 4.4.
Пример 4.4. Использование модуля
threading (mtsleepC.py)
В классе Thread из модуля threading предусмотрен метод join(), который по- зволяет обеспечить ожидание в основном потоке завершения текущего потока.
1 #!/usr/bin/env python
2 3 import threading
4 from time import sleep, ctime
5 6 loops = [4,2]
7 8 def loop(nloop, nsec):
9 print 'start loop', nloop, 'at:', ctime()
10 sleep(nsec)
11 print 'loop', nloop, 'done at:', ctime()
12 13 def main():
14 print 'starting at:', ctime()
15 threads = []
16 nloops = range(len(loops))
17 18 for i in nloops:
19 t = threading.Thread(target=loop,
20 args=(i, loops[i]))
21 threads.append(t)
22 23 for i in nloops: # запуск потоков
24 threads[i].start()
25 26 for i in nloops: # ожидание завершения
27 threads[i].join() # всех потоков
28 29 print 'all DONE at:', ctime()
30 31 if __name__ == '__main__':
32 main()
После выполнения сценария, приведенного в примере 4.4, формируется пример- но такой же вывод, как и при вызове предыдущих сценариев:
06_ch04.indd 197 22.01.2015 22:00:41
Глава 4
Многопоточное программирование
198
$ mtsleepC.py starting at: Sun Aug 13 18:16:38 2006 start loop 0 at: Sun Aug 13 18:16:38 2006 start loop 1 at: Sun Aug 13 18:16:38 2006 loop 1 done at: Sun Aug 13 18:16:40 2006 loop 0 done at: Sun Aug 13 18:16:42 2006 all DONE at: Sun Aug 13 18:16:42 2006
Так что же фактически изменилось? Удалось избавиться от блокировок, которые приходилось реализовывать при использовании модуля thread. Вместо этого соз- дается ряд объектов Thread. После создания экземпляра каждого объекта Thread остается лишь передать функцию (target) и параметры (args) и получить взамен экземпляр Thread. Наибольшее различие между созданием экземпляра Thread (пу- тем вызова Thread()) и вызовом thread.start_new_thread() состоит в том, что в первом случае запуск нового потока не происходит немедленно. Это удобно с точки зрения синхронизации, особенно если не требуется, чтобы потоки запускались сразу после их создания.
Иными словами, появляется возможность почти одновременно запустить все потоки по окончании их распределения, но не раньше, для чего остается лишь вы- звать метод start() каждого потока. Кроме того, отпадает необходимость зани- маться управлением целым рядом блокировок (выделением, захватом, освобожде- нием, проверкой состояния блокировки и т.д.), поскольку достаточно лишь вызвать метод join() для каждого потока. Метод join() обеспечивает переход в состояние ожидания до завершения работы потока или до истечения тайм-аута, если он пред- усмотрен. Использование метода join() открывает путь к созданию гораздо более наглядных программ по сравнению с применением бесконечного цикла, в котором происходит ожидание освобождения блокировок (такие блокировки иногда имену- ются спин-блокировками, или “крутящимися” блокировками, именно по той причине, что применяются в бесконечном цикле).
Еще одной важной отличительной особенностью метода join() является то, что он вообще не требует вызова. После запуска потока его выполнение происходит до завершения переданной ему функции, после чего осуществляется выход из потока.
Если в основном потоке должны быть выполнены какие-то другие действия, кроме ожидания завершения потоков (такие как дополнительная обработка или ожидание новых клиентских запросов), организовать это совсем несложно. Метод join() ста- новится удобным, только если требуется обеспечить ожидание завершения потока.
Создание экземпляра
Thread и передача
вызываемого экземпляра класса
Подход, аналогичный передаче функции при создании потока, состоит в приме- нении вызываемого класса и передаче его экземпляра на выполнение; в этом состоит в большей степени объектно-ориентированный способ многопоточного программи- рования. Такой вызываемый класс воплощает в себе среду выполнения, а это откры- вает намного больше возможностей по сравнению с применением функции или выбором из ряда функций. Теперь в руках у программиста оказывается вся мощь объекта класса, а не просто единственной функции или даже ряда функций, опреде- ляемого списком или кортежем.
06_ch04.indd 198 22.01.2015 22:00:41