Файл: Многопоточное программированиеВ этой главе.pdf

ВУЗ: Не указан

Категория: Не указан

Дисциплина: Не указана

Добавлен: 12.12.2023

Просмотров: 100

Скачиваний: 3

ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.

199 4.5. Модуль threading
После введения в код нового класса ThreadFunc и внесения других небольших из- менений в сценарий mtsleepC.py был создан сценарий mtsleepD.py, показанный в примере 4.5.
Пример 4.5. Использование вызываемых классов (
mtsleepD.py)
В этом примере передается вызываемый класс (экземпляр), в отличие от отдель- ной функции. Такой подход является в большей степени объектно-ориентированным по сравнению с применяемым в mtsleepC.py.
1 #!/usr/bin/env python
2 3 import threading
4 from time import sleep, ctime
5 6 loops = [4,2]
7 8 class ThreadFunc(object):
9 10 def __init__(self, func, args, name=''):
11 self.name = name
12 self.func = func
13 self.args = args
14 15 def __call__(self):
16 self.func(*self.args)
17 18 def loop(nloop, nsec):
19 print 'start loop', nloop, 'at:', ctime()
20 sleep(nsec)
21 print 'loop', nloop, 'done at:', ctime()
22 23 def main():
24 print 'starting at:', ctime()
25 threads = []
26 nloops = range(len(loops))
27 28 for i in nloops: # создание всех потоков
29 t = threading.Thread(
30 target=ThreadFunc(loop, (i, loops[i]),
31 loop.__name__))
32 threads.append(t)
33 34 for i in nloops: # запуск всех потоков
35 threads[i].start()
36 37 for i in nloops: # ожидание завершения
38 threads[i].join()
39 40 print 'all DONE at:', ctime()
41 42 if __name__ == '__main__':
43 main()
После вызова на выполнение сценария mtsleepD.py формируется ожидаемый вывод:
06_ch04.indd 199 22.01.2015 22:00:41

Глава 4

Многопоточное программирование
200
$ mtsleepD.py starting at: Sun Aug 13 18:49:17 2006 start loop 0 at: Sun Aug 13 18:49:17 2006 start loop 1 at: Sun Aug 13 18:49:17 2006 loop 1 done at: Sun Aug 13 18:49:19 2006 loop 0 done at: Sun Aug 13 18:49:21 2006 all DONE at: Sun Aug 13 18:49:21 2006
Так что же изменилось на этот раз? Произошло добавление класса ThreadFunc и внесены небольшие изменения в процедуру создания экземпляра объекта Thread, в которой также порождается ThreadFunc, новый вызываемый класс. Фактически в данном примере применяется процедура создания не одного, а двух экземпляров.
Рассмотрим класс ThreadFunc более подробно.
Этот класс должен быть достаточно общим, для того чтобы его можно было ис- пользовать не только с функцией loop(), но и с другими функциями, поэтому была добавлена некоторая новая инфраструктура, которая обеспечивает хранение этим классом параметров для функции, самой функции, а также строки с именем функ- ции. Конструктор __init__() лишь задает все необходимые значения.
При вызове в коде Thread объекта ThreadFunc в связи с созданием нового пото- ка вызывается специальный метод __call__(). Необходимый набор параметров уже задан, поэтому его не обязвтельно передавать конструктору Thread() и можно вызы- вать функцию непосредственно.
Подкласс
Thread и создание экземпляра подкласса
В последнем вводном примере рассмотрим создание подкласса Thread(). Как оказалось, применяемая при этом последовательность действий весьма напоминает то, что происходит при создании вызываемого класса, как в предыдущем примере.
Код создания подкласса является немного более легким для восприятия, если дело касается создания потоков (строки 29-30). Код сценария mtsleepE.py представлен в примере 4.6, затем показан вывод, полученный в результате выполнения этого сцена- рия, а сравнение сценариев mtsleepE.py и mtsleepD.py оставлено в качестве упраж- нения для читателя.
Пример 4.6. Создание подкласса
Thread (mtsleepE.py)
Вместо создания экземпляра класса Thread создается его подкласс. Благодаря это- му открываются более широкие возможности настройки объектов многопоточной поддержки и упрощается осуществление действий по созданию потока.
1 #!/usr/bin/env python
2 3 import threading
4 from time import sleep, ctime
5 6 loops = (4, 2)
7 8 class MyThread(threading.Thread):
9 def __init__(self, func, args, name=''):
10 threading.Thread.__init__(self)
11 self.name = name
12 self.func = func
13 self.args = args
06_ch04.indd 200 22.01.2015 22:00:42


201 4.5. Модуль threading
14 15 def run(self):
16 self.func(*self.args)
17 18 def loop(nloop, nsec):
19 print 'start loop', nloop, 'at:', ctime()
20 sleep(nsec)
21 print 'loop', nloop, 'done at:', ctime()
22 23 def main():
24 print 'starting at:', ctime()
25 threads = []
26 nloops = range(len(loops))
27 28 for i in nloops:
29 t = MyThread(loop, (i, loops[i]),
30 loop.__name__)
31 threads.append(t)
32 33 for i in nloops:
34 threads[i].start()
35 36 for i in nloops:
37 threads[i].join()
38 39 print 'all DONE at:', ctime()'
40 41 if __name__ == '__main__':
42 main()
Ниже приведен вывод сценария mtsleepE.py. В данном случае полученные ре- зультаты вполне соответствуют ожиданию:
$ mtsleepE.py starting at: Sun Aug 13 19:14:26 2006 start loop 0 at: Sun Aug 13 19:14:26 2006 start loop 1 at: Sun Aug 13 19:14:26 2006 loop 1 done at: Sun Aug 13 19:14:28 2006 loop 0 done at: Sun Aug 13 19:14:30 2006 all DONE at: Sun Aug 13 19:14:30 2006
Сравнивая исходный код модулей mtsleep4 и mtsleep5, необходимо подчеркнуть наиболее значительные отличия: во-первых, в конструкторе подкласса MyThread приходится вначале вызывать конструктор базового класса (строка 9), и, во-вторых, применявшийся ранее специальный метод __call__() должен получить в подклассе имя run().
После этого дополним класс MyThread некоторыми средствами формирования диагностического вывода и сохраним его в отдельном модуле myThread (как показано в примере 4.7). В следующих примерах этот класс будет применяться для импорта.
Вместо того чтобы просто вызывать применяемые функции, сохраним результат в атрибуте экземпляра self.res и создадим новый метод для получения этого значе- ния, getResult().
06_ch04.indd 201 22.01.2015 22:00:42

Глава 4

Многопоточное программирование
202
Пример 4.7. Подкласс
MyThread потока (myThread.py)
Для того чтобы повысить общность подкласса Thread из сценария mtsleepE.py, переместим этот подкласс в отдельный модуль и добавим метод getResult() для вызова функций, которые формируют возвращаемые значения.
1 #!/usr/bin/env python
2 3 import threading
4 from time import ctime
5 6 class MyThread(threading.Thread):
7 def __init__(self, func, args, name=''):
8 threading.Thread.__init__(self)
9 self.name = name
10 self.func = func
11 self.args = args
12 13 def getResult(self):
14 return self.res
15 16 def run(self):
17 print 'starting', self.name, 'at:', \
18 ctime()
19 self.res = self.func(*self.args)
20
1   2   3   4   5   6   7   8

print self.name, 'finished at:', \
21 ctime()
4.5.2. Другие функции модуля
Threading
В модуле Threading, кроме различных объектов синхронизации и обеспечения многопоточной поддержки, предусмотрены также некоторые поддерживающие функции, представленные в табл. 4.4.
Таблица 4.4. Функции модуля
threading
Функция
Описание
activeCount/active_count()
a
Возвращает количество активных в настоящее время объ- ектов
Thread currentThread()/current_thread a
Возвращает текущий объект
Thread enumerate()
Возвращает список всех активных в настоящее время объ- ектов
Thread settrace(func)
b
Задает функцию трассировки для всех потоков setprofile(func)
b
Задает профиль
function для всех потоков stack_size(size=0)
c
Возвращает размер стека вновь созданных потоков; с учетом потоков, которые будут создаваться в дальнейшем, может быть задан необязательный параметр
size
a
Имена в так называемом ВерблюжьемСтиле рассматриваются как устаревшие и заменя- ются, начиная с версии Python 2.6.
b
Новое в версии Python 2,3.
c
Псевдоним метода thread.stack_size(). Метод и его псевдоним впервые введены в версии Python 2.5.
06_ch04.indd 202 22.01.2015 22:00:42

203 4.6. Сравнение однопоточного и многопоточного выполнения
4.6. Сравнение однопоточного
и многопоточного выполнения
В сценарии mtfacfib.py, представленном в примере 4.8, происходит сравнение хода выполнения рекурсивных функций, включая функции вычисления числа Фибо- наччи, факториала и суммы. В этом сценарии все три функции выполняются в од- нопоточном режиме. После этого та же задача решается с использованием потоков, что позволяет показать одно из преимуществ применения среды многопоточной поддержки.
Пример 4.8. Функции вычисления числа Фибоначчи, факториала и суммы (
mtfacfib.py)
В этом многопоточном приложении выполняются три отдельные рекурсивные функции, сначала в однопоточном режиме, а затем с применением альтернативной организации работы с несколькими потоками.
1 #!/usr/bin/env python
2 3 from myThread import MyThread
4 from time import ctime, sleep
5 6 def fib(x):
7 sleep(0.005)
8 if x < 2: return 1 9 return (fib(x-2) + fib(x-1))
10 11 def fac(x):
12 sleep(0.1)
13 if x < 2: return 1 14 return (x * fac(x-1))
15 16 def sum(x):
17 sleep(0.1)
18 if x < 2: return 1 19 return (x + sum(x-1))
20 21 funcs = [fib, fac, sum]
22 n = 12 23 24 def main():
25 nfuncs = range(len(funcs))
26 27 print '*** SINGLE THREAD'
28 for i in nfuncs:
29 print 'starting', funcs[i].__name__, 'at:', \
30 ctime()
31 print funcs[i](n)
32 print funcs[i].__name__, 'finished at:', \
33 ctime()
34 35 print '\n*** MULTIPLE THREADS'
36 threads = []
37 for i in nfuncs:
38 t = MyThread(funcs[i], (n,),
39 funcs[i].__name__)
40 threads.append(t)
06_ch04.indd 203 22.01.2015 22:00:43


Глава 4

Многопоточное программирование
204 41 42 for i in nfuncs:
43 threads[i].start()
44 45 for i in nfuncs:
46 threads[i].join()
47 print threads[i].getResult()
48 49 print 'all DONE'
50 51 if __name__ == '__main__':
52 main()
Выполнение в однопоточном режиме сводится к тому, что функции вызываются одна за другой и после завершения их работы сразу же отображаются полученные результаты вызова.
Если же выполнение функций происходит в многопоточном режиме, то результат не отображается немедленно. Желательно, чтобы класс MyThread был настолько об- щим, насколько это возможно (способным выполнять вызываемые функции, которые формируют и не формируют вывод), поэтому вызов метода getResult() отклады- вается до самого конца, что позволяет показать значения, возвращенные в каждом вызове функции, после того, как все будет сделано.
В данном примере вызовы всех функций выполняются очень быстро (вернее, исключением может стать вызов функции вычисления числа Фибоначчи), поэто- му, как можно заметить, мы были вынуждены добавить вызовы sleep() к каждой функции для замедления процесса выполнения, чтобы можно было убедиться в том, что многопоточная организация действительно способствует повышению произво- дительности, если в разных потоках применяются функции с различным временем выполнения. Безусловно, на практике дополнять функции вызовами sleep() нет не- обходимости. Так или иначе, получим такой вывод:
$ mtfacfib.py
*** SINGLE THREAD starting fib at: Wed Nov 16 18:52:20 2011 233 fib finished at: Wed Nov 16 18:52:24 2011 starting fac at: Wed Nov 16 18:52:24 2011 479001600 fac finished at: Wed Nov 16 18:52:26 2011 starting sum at: Wed Nov 16 18:52:26 2011 78 sum finished at: Wed Nov 16 18:52:27 2011
*** MULTIPLE THREADS starting fib at: Wed Nov 16 18:52:27 2011 starting fac at: Wed Nov 16 18:52:27 2011 starting sum at: Wed Nov 16 18:52:27 2011 fac finished at: Wed Nov 16 18:52:28 2011 sum finished at: Wed Nov 16 18:52:28 2011 fib finished at: Wed Nov 16 18:52:31 2011 233 479001600 78 all DONE
06_ch04.indd 204 22.01.2015 22:00:43

205 4.7. Практическое применение многопоточной обработки
4.7. Практическое применение
многопоточной обработки
До сих пор были представлены лишь упрощенные, применяемые в качестве при- меров фрагменты кода, которые весьма далеки от того, что должно применяться в реальном приложении. Фактически единственное назначение этих примеров состоит лишь в том, чтобы показать потоки в работе и продемонстрировать различные спо- собы их создания. При этом во всех примерах запуск потоков и ожидание их завер- шения происходит почти одинаково, а действия, выполняемые потоками, главным образом сводятся к приостановке.
Кроме того, как уже было сказано в разделе 4.3.1, виртуальная машина Python в действительности работает в однопоточном режиме (с применением глобальной блокировки интерпретатора), поэтому достижение большего распараллеливания в программе Python возможно, только если многопоточная организация применя- ется в приложении, ограничиваемом пропускной способностью ввода-вывода, а не пропускной способностью процессора, в котором так или иначе происходит лишь циклическая передача управления от одного процесса к другому. По этой причине мы рассмотрим пример приложения первого типа и в качестве дальнейшего упраж- нения попытаемся перенести его в версию Python 3, чтобы можно было понять, что с этим связано.
4.7.1. Пример ранжирования книг
Сценарий bookrank.py, показанный в примере 4.9, весьма прост. Он выполняет переход на сайт интернет-торговли Amazon и запрашивает текущее ранжирование книг, написанных вашим покорным слугой. В рассматриваемом примере кода пред- ставлены функция getRanking(), в которой используется регулярное выражение для извлечения и возврата текущего ранжирования, и функция showRanking(), которая отображает результаты для пользователя.
Следует отметить, что согласно условиям использования “компания Amazon предо-
ставляет пользователю сайта ограниченные права доступа к сайту и использования его в
личных целях, но не позволяет загружать содержимое сайта (исключая кэширование стра-
ниц) либо изменять его или любую его часть без явно выраженного письменного согласия
Amazon.” Задача рассматриваемого приложения состоит в том, чтобы выбрать данные о текущем ранжировании конкретной книги, а затем отбросить остальные сведения о ранжировании; в данном случае даже кеширование страницы не применяется.
В примере 4.9 представлена первая (и почти последняя) попытка создания сцена- рия bookrank.py, который не относится к многопоточной версии.
Пример 4.9. Программа извлечения с веб-страницы данных о ранжировании книг
(
bookrank.py)
В этом сценарии выполняются вызовы, обеспечивающие загрузку информации о ранжировании книг через отдельные потоки.
1 #!/usr/bin/env python
2 3 from atexit import register
4 from re import compile
5 from threading import Thread
06_ch04.indd 205 22.01.2015 22:00:43


Глава 4

Многопоточное программирование
206 6 from time import ctime
7 from urllib2 import urlopen as uopen
8 9 REGEX = compile('#([\d,]+) in Books ')
10 AMZN = 'http://amazon.com/dp/'
11 ISBNs = {
12 '0132269937': 'Core Python Programming',
13 '0132356139': 'Python Web Development with Django',
14 '0137143419': 'Python Fundamentals',
15: }
16:
17: def getRanking(isbn):
18: page = uopen('%s%s' % (AMZN, isbn)) #или str.format()
19: data = page.read()
20: page.close()
21: return REGEX.findall(data)[0]
22:
23: def _showRanking(isbn):
24: print '- %r ranked %s' % (
25: ISBNs[isbn], getRanking(isbn))
26:
27: def _main():
28: print 'At', ctime(), 'on Amazon...'
29: for isbn in ISBNs:
30: _showRanking(isbn)
31:
32: @register
33: def _atexit():
34: print 'all DONE at:', ctime()
35:
36: if __name__ == '__main__':
37: main()
Построчное объяснение
Строки 1–7
Это строки запуска и импорта. Для определения того, когда будет завершено вы- полнение сценария, используется функция atexit.register() (почему это сделано, будет описано ниже). Кроме того, для работы с шаблоном, с помощью которого из- влекаются сведения о ранжировании книг со страниц с описанием товаров на сайте
Amazon, применяется функция поддержки регулярных выражений re.compile().
Затем результаты импорта threading.Thread сохраняются для использования в на- меченном на будущее усовершенствованном варианте сценария (об этом немного позже), вызывается метод time.ctime() для получения строки с текущей отметкой времени, а для получения доступа к каждой ссылке применяется метод urllib2.
urlopen().
Строки 9–15
В этом сценарии используются три константы. Первой из них является REGEX, объект регулярного выражения (полученный путем компиляции шаблона регуляр- ного выражения, который согласуется с данными по ранжированию книги); вторая константа — AMZN, префикс каждой ссылки на товары Amazon. За этим префиксом следует ISBN (International Standard Book Number) искомой книги; ISBN служит для
06_ch04.indd 206 22.01.2015 22:00:44

207 4.7. Практическое применение многопоточной обработки книги уникальным обозначением и позволяет отличить ее от других. Предусмотрены два стандарта ISBN: ISBN-10, с десятисимвольным обозначением, и ISBN-13, принятый позднее стандарт, в котором применяются тринадцать символов. В настоящее время поисковая система Amazon распознает ISBN обоих типов, поэтому для краткости бу- дем использовать только ISBN-10. Искомые ISBN хранятся в словаре ISBNs (который представляет собой третью константу) наряду с соответствующими названиями книг.
Строки 17–21
Функция getRanking() предназначена для получения ISBN, создания конеч- ного значения URL, которое применяется для доступа к серверам Amazon, а затем вызова для этого URL метода urllib2.urlopen(). Для соединения воедино компо- нентов значения URL используется оператор форматирования строки (в строке 18), но если работа ведется с версией Python 2.6 или последующей версией, то можно также попытаться воспользоваться методом str.format(), например '{0}{1}'.
format(AMZN,isbn).
После получения полного URL вызывается метод urllib2.urlopen() (в данном сценарии в качестве него применяется сокращение uopen()), который в случае успе- ха возвращает файловый объект после ответа веб-сервера. Затем происходит вызов функции read() для загрузки всей веб-страницы, и файловый объект закрывается.
Если регулярное выражение составлено правильно, то при его использовании долж- но происходить одно и только одно сопоставление, поэтому достаточно извлечь не- обходимый результат из сформированного списка (все остальные результаты будут пропущены) и возвратить его в вызывающую функцию.
Строки 23–25
Функция _showRanking() представляет собой всего лишь короткий фрагмент кода, в котором берется ISBN, осуществляется поиск названия книги, которую пред- ставляет этот ISBN, вызывается метод getRanking() для получения текущего ранга книги на веб-сайте Amazon, после чего ISBN и название передаются пользователю.
В имени этого метода применяется префикс в виде одного знака подчеркивания.
Этот префикс служит в качестве указания, что данная функция является специаль- ной, предназначена для использования только в данном модуле и не должна быть импортирована в каком-либо другом приложении в составе библиотечного или вспо- могательного модуля.
Строки 27–30
Функция _main() также относится к категории специальных и вызывается на вы- полнение, только если данный модуль вызван непосредственно из командной стро- ки (а не импортируется для использования другим модулем). Происходит отобра- жение времени начала и окончания (чтобы пользователь мог определить, сколько времени потребовалось для выполнения всего сценария), затем вызывается функции
_showRanking() применительно к каждому ISBN для поиска и отображения данных о текущем ранжировании каждой книги на сайте Amazon.
Строки 32–37
В этих строках выполняются действия, которые прежде нами не рассматривались.
Рассмотрим назначение функции atexit.register(). Такие функции принято на- зывать декораторами. Декораторы — одна из разновидностей служебных функций.
06_ch04.indd 207 22.01.2015 22:00:44