Многопоточное программированиеВ этой главе
Скачать 0.74 Mb.
|
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 Глава 4 Многопоточное программирование 208 В данном случае с помощью указанной функции происходит регистрация функции выхода в интерпретаторе Python. Это равносильно запросу, чтобы интерпретатор вызвал некоторую специальную функцию непосредственно перед завершением сце- нария. (Вместо вызова декоратора можно также применить конструкцию register (_atexit()). Рассмотрим причины использования декоратора в данном коде. Прежде всего не- обходимо отметить, что можно было бы вполне обойтись без него. Оператор вывода может быть размещен в последней части функции _main(), в строках 27–31, но в действительности при этом организация программ оставляла бы желать лучшего. Кроме того, здесь демонстрируется пример применения функционального средства, без которого при определенных обстоятельствах нельзя было бы обойтись в при- ложении, применяемом на производстве. Предполагается, что читатель сам сможет определить назначение строк 36-37, поэтому перейдем к описанию полученного вывода: $ python bookrank.py At Wed Mar 30 22:11:19 2011 PDT on Amazon... - 'Core Python Programming' ranked 87,118 - 'Python Fundamentals' ranked 851,816 - 'Python Web Development with Django' ranked 184,735 all DONE at: Wed Mar 30 22:11:25 2011 Заслуживает внимания то, что в данном примере разделены процессы получения данных (getRanking()) и их отображения (_showRanking() и _main()), что позволя- ет при желании вместо отображения результатов для пользователя с помощью тер- минала предусмотреть какой-то другой способ обработки вывода. На практике мо- жет потребоваться отправить эти данные назад с помощью веб-шаблона, сохранить в базе данных, вывести в виде текста на экран мобильного телефона и т.д. Если бы весь этот код был помещен в одну функцию, то было бы сложнее обеспечить его повтор- ное использование и (или) отправить по другому назначению. Кроме того, если компания Amazon изменит компоновку своих страниц с опи- санием товаров, то может потребоваться лишь изменить регулярное выражение, предназначенное для выборки данных с веб-страниц, чтобы по-прежнему иметь воз- можность извлекать сведения о книгах. Следует также отметить, что в этом простом примере вполне оправдывает себя способ обработки данных с помощью регулярного выражения (который может быть даже заменен простыми традиционными операци- ями работы со строками), но в какое-то время может потребоваться более мощный синтаксический анализатор разметки, такой как HTMLParser из стандартной библио- теки, а возможно, нельзя будет обойтись без таких инструментов сторонних произво- дителей, как BeautifulSoup, html5lib или lxml. (Некоторые из этих инструментов будут продемонстрированы в главе 9.) Добавление в программу средств многопоточной поддержки Очевидно, что приведенный выше пример все еще относится к категории неслож- ных однопоточных программ. Теперь перейдем к внесению изменений в приложение, чтобы в нем вместо этого использовались потоки. Это приложение, ограничиваемое пропускной способностью ввода-вывода, поэтому вполне подходит для применения в нем многопоточной организации. Для упрощения на первых порах мы не будем использовать классы и объектно-ориентированное программирование; вместо этого в программе будет применяться непосредственно метод threading.Thread, поэтому 06_ch04.indd 208 22.01.2015 22:00:44 209 4.7. Практическое применение многопоточной обработки данный пример в большей степени должен напоминать сценарий mtsleepC.py, чем любой из последующих примеров. Дополнением является лишь то, что в приложе- нии создаются потоки, которые немедленно запускаются. Возьмем за основу ранее разработанное приложение и изменим вызов _ showRanking(isbn) следующим образом: Thread(target=_showRanking, args=(isbn,)).start(). Получен именно такой результат, который требуется! Теперь в нашем распоря- жении имеется окончательная версия сценария bookrank.py, которая показывает, что это приложение (как правило) выполняется быстрее, чем предыдущее, благодаря дополнительному распараллеливанию. Тем не менее быстродействие приложения ограничивается тем, насколько быстро будет получен ответ, обработка которого по- требовала больше всего времени. $ python bookrank.py At Thu Mar 31 10:11:32 2011 on Amazon... - 'Python Fundamentals' ranked 869,010 - 'Core Python Programming' ranked 36,481 - 'Python Web Development with Django' ranked 219,228 all DONE at: Thu Mar 31 10:11:35 2011 Как показывает полученный вывод, вместо шести секунд, в течение которых вы- полнялась однопоточная версия, для многопоточной достаточно трех. Кроме того, важно отметить, что в окончательно полученном выводе последовательность резуль- татов может изменяться в зависимости от времени завершения работы потоков, в от- личие от однопоточной версии. В версии, в которой не применялась многопоточная организация, последовательность расположения результатов всегда определяется ключами словаря, а теперь все запросы выполняются параллельно и вывод формиру- ется в той последовательности, которая определяется значениями времени заверше- ния отдельных потоков. В предыдущих примерах (в сценариях mtsleepX.py) применительно ко всем по- токам вызывался метод Thread.join() для блокирования выполнения до заверше- ния каждого потока. Это равносильно блокированию дальнейшей работы основного потока до завершения всех потоков, поэтому инструкция вывода на печать “all DONE at” вызывается после того, как действительно закончится вся работа. В указанных примерах не было необходимости применять метод join() ко всем потокам, поскольку ни один из потоков не функционировал в качестве демона. В ос- новном потоке не происходит выход из сценария до тех пор, пока не произойдет успешное или неудачное завершение всех порожденных потоков. Опираясь на эти рассуждения, мы удалили все вызовы join() из сценария mtsleepF.py. Тем не менее следует учитывать, что неправильно было бы отображать строку “all done” (все сдела- но) на том же этапе выполнения сценария, как и прежде. Строка “all done” должна быть выведена в основном потоке, т.е. до завершения дочерних потоков, чтобы не было необходимости вызывать оператор печати, выше функции _main(). Этот оператор print можно поместить в одно из двух мест в сце- нарии: после строки 37, где происходит возврат из функции _main() (самая послед- няя строка, выполняемая в сценарии), или в месте, которое определяется в связи с использованием метода atexit.register() для регистрации функции выхода. Эта тема в настоящей книге еще не рассматривалась, и в дальнейшем мы к ней обяза- тельно вернемся, но именно здесь удобно впервые затронуть вопрос регистрации 06_ch04.indd 209 22.01.2015 22:00:44 |