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

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

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

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

Добавлен: 12.12.2023

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

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

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

Глава 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


Глава 4

Многопоточное программирование
210
функций. Важно также, что для регистрации функций применяется интерфейс, ко- торый останется неизменным после перехода от Python 2 к Python 3 (это — тема сле- дующего раздела).
1   2   3   4   5   6   7   8

Перенос приложения в версию Python 3
Теперь перейдем к рассмотрению еще одного варианта данного сценария, который предназначен для работы с версией Python 3. С этой темой необ- ходимо ознакомиться, изучая пути переноса проектов и приложений из те- кущей версии интерпретатора Python в последующую версию. К счастью, эту работу не требуется выполнять вручную, поскольку уже предусмотрены необходимые инструменты, одним из которых является инструмент 2to3.
Вообще говоря, предусмотрены два способа его использования:
$ 2to3 foo.py # в выводе показаны только различия
$ 2to3 -w foo.py # переопределяет с помощью кода версии 3.x
В первой команде инструмент 2to3 лишь отображает различия между исходным сценарием в версии 2.x и сформированным с его помощью эквивалентом для версии
3.x. Флаг -w служит для инструмента 2to3 указанием, что исходный сценарий должен быть перезаписан вновь полученным сценарием для версии 3.x, а сценарий для вер- сии 2.x переименован в foo.py.bak.
Вызовем на выполнение инструмент 2to3 применительно к файлу сценария bookrank.py с перезаписью существующего файла. Предусмотрен не только вывод различий; сохраняется также новая версия, как уже было сказано:
$ 2to3 -w bookrank.py
RefactoringTool: Skipping implicit fixer: buffer
RefactoringTool: Skipping implicit fixer: idioms
RefactoringTool: Skipping implicit fixer: set_literal
RefactoringTool: Skipping implicit fixer: ws_comma
--- bookrank.py (original)
+++ bookrank.py (refactored)
@@ -4,7 +4,7 @@ from re import compile from threading import Thread from time import ctime
-from urllib2 import urlopen as uopen
+from urllib.request import urlopen as uopen
REGEX = compile('#([\d,]+) in Books ')
AMZN = 'http://amazon.com/dp/'
@@ -21,17 +21,17 @@ return REGEX.findall(data)[0] def _showRanking(isbn):
- print '- %r ranked %s' % (
- ISBNs[isbn], getRanking(isbn))
+ print('- %r ranked %s' % (
+ ISBNs[isbn], getRanking(isbn))) def _main():
- print 'At', ctime(), 'on Amazon...'
06_ch04.indd 210 22.01.2015 22:00:45

211 4.7. Практическое применение многопоточной обработки
+ print('At', ctime(), 'on Amazon...') for isbn in ISBNs:
Thread(target=_showRanking, args=(isbn,)).start()#_showRanking(isbn)
@register def _atexit():
- print 'all DONE at:', ctime()
+ print('all DONE at:', ctime()) if __name__ == '__main__':
_main()
RefactoringTool: Files that were modified:
RefactoringTool: bookrank.py
Следующий шаг читатели могут рассматривать как необязательный. Достаточно лишь отметить, что в нем рассматриваемые файлы были переименованы в bookrank.
py и bookrank3.py с использованием команд POSIX (пользователи компьютеров с операционной системой Windows должны использовать команду ren):
$ mv bookrank.py bookrank3.py
$ mv bookrank.py.bak bookrank.py
Разумеется, было бы желательно, чтобы преобразование сценария для использо- вания в новой версии интерпретатора прошло идеально, чтобы не пришлось ни о чем заботиться, приступая к работе со сценарием нового поколения. Однако в дан- ном случае произошло нечто непредвиденное и в каждом потоке возникает исключе- ние (приведенный вывод относится только к одному потоку; нет смысла показывать результаты для других потоков, поскольку они являются такими же):
$ python3 bookrank3.py
Exception in thread Thread-1:
Traceback (most recent call last):
File "/Library/Frameworks/Python.framework/Versions/
3.2/lib/python3.2/threading.py", line 736, in
_bootstrap_inner self.run()
File "/Library/Frameworks/Python.framework/Versions/
3.2/lib/python3.2/threading.py", line 689, in run self._target(*self._args, **self._kwargs)
File "bookrank3.py", line 25, in _showRanking
ISBNs[isbn], getRanking(isbn)))
File "bookrank3.py", line 21, in getRanking return REGEX.findall(data)[0]
TypeError: can't use a string pattern on a bytes-like object
:
Что же случилось? По-видимому, проблема заключается в том, что регулярное выражение представлено в виде строки Юникода, тогда как данные, полученные с помощью метода read() файлового объекта (возвращенного функцией urlopen()), имеют вид строки ASCII/bytes. Чтобы исправить эту ошибку, откомпилируем вме- сто текстовой строки объект bytes. Для этого внесем изменения в строку 9, чтобы в методе re.compile() производилась компиляция строки bytes (добавим строку bytes). Для этого добавим обозначение b строки bytes непосредственно перед от- крывающей кавычкой следующим образом:
06_ch04.indd 211 22.01.2015 22:00:45


Глава 4

Многопоточное программирование
212
REGEX = compile(b'#([\d,]+) in Books ')
Now let's try it again:
$ python3 bookrank3.py
At Sun Apr 3 00:45:46 2011 on Amazon...
- 'Core Python Programming' ranked b'108,796'
- 'Python Web Development with Django' ranked b'268,660'
- 'Python Fundamentals' ranked b'969,149' all DONE at: Sun Apr 3 00:45:49 2011
Опять что-то не так! Что же случилось теперь? Безусловно, результат стал немного лучше (нет ошибок), но выглядит странно. В данных ранжирования, полученных с помощью регулярных выражений, после передачи в функцию str() отображаются символы b и кавычки. Для устранения этого недостатка первым побуждением может стать попытка применить операцию получения среза строки, которая также выгля- дит довольно неуклюже:
>>> x = b'xxx'
>>> repr(x)
"b'xxx'"
>>> str(x)
"b'xxx'"
>>> str(x)[2:-1]
'xxx'
Тем не менее более подходящий вариант состоит в применении операции преоб- разования данных в действительное значение (строка в Юникоде, возможно, с исполь- зованием UTF-8):
>>> str(x, 'utf-8')
'xxx'
Для реализации этого решения в текущем сценарии внесем аналогичное измене- ние в строку 53, чтобы она выглядела следующим образом:
return str(REGEX.findall(data)[0], 'utf-8')
После этого вывод сценария для версии Python 3 полностью совпадает с тем, что получен в сценарии Python 2:
$ python3 bookrank3.py
At Sun Apr 3 00:47:31 2011 on Amazon...
- 'Python Fundamentals' ranked 969,149
- 'Python Web Development with Django' ranked 268,660
- 'Core Python Programming' ranked 108,796 all DONE at: Sun Apr 3 00:47:34 2011
Вообще говоря, практика показывает, что перенос сценария из версии 2.x в вер- сию 3.x осуществляется по аналогичному принципу: необходимо убедиться, что код проходит все тесты модульности и интеграции, провести основное преобразование с использованием инструмента 2to3 (и других инструментов), а затем устранить воз- можные расхождения, добиваясь того, чтобы код успешно выполнялся и проходил такие же проверки, как и исходный сценарий. Попробуем повторить это упражнение снова на следующем примере, в котором демонстрируется использование синхрони- зации с помощью потоков.
06_ch04.indd 212 22.01.2015 22:00:45

213 4.7. Практическое применение многопоточной обработки
4.7.2. Примитивы синхронизации
В основной части этой главы рассматривались основные концепции многопоточ- ной организации и было показано, как использовать многопоточность в приложе- ниях Python. Однако в этом изложении не затрагивался один очень важный аспект многопоточного программирования: синхронизация. Довольно часто в многопоточ- ном коде содержатся определенные функции или блоки, в которых необходимо (или желательно) ограничить количество выполняемых потоков до одного. Обычно такие ситуации обнаруживаются при внесении изменений в базу данных, обновлении фай- ла или выполнении подобных действий, при которых может возникнуть состояние состязания. Как уже было сказано в этой главе, такое состояние проявляется, если код допускает появление нескольких путей выполнения или вариантов поведения либо формирование несогласованных данных, если один поток будет запущен раньше дру- гого, или наоборот. (С дополнительными сведениями о состояниях состязания мож- но ознакомиться на странице http://en.wikipedia.org/wiki/Race_condition.)
В таких случаях возникает необходимость обеспечения синхронизации. Синхро- низация должна использоваться, если к какому-то из критических участков кода мо- гут подойти одновременно несколько потоков (см. http://en.wikipedia.org/wiki/
Critical_section), но в каждый конкретный момент времени должно быть разре- шено дальнейшее выполнение только одного потока. Программист регламентиру- ет прохождение потоков и для управления ими выбирает подходящие примитивы синхронизации, или механизмы управления потоками, с помощью которых вводит в действие синхронизацию. Предусмотрено несколько различных методов синхрониза- ции процессов (см. http://en.wikipedia.org/wiki/Synchronization_(computer_
science)), часть которых поддерживается языком Python. Эта поддержка предо- ставляет достаточно возможностей для выбора метода, наиболее подходящего для конкретной задачи.
Методы синхронизации уже были представлены ранее, в начале этого раздела, по- этому перейдем к рассмотрению нескольких примеров сценариев, в которых исполь- зуются примитивы синхронизации двух типов: блокировки/мьютексы и семафоры.
Блокировка относится к числу самых простых среди всех механизмов синхронизации и находится на самом низком уровне, а семафоры предназначены для применения в таких ситуациях, в которых несколько потоков конкурируют друг с другом, стремясь получить доступ к ограниченным ресурсам. Понять назначение блокировок проще, поэтому начнем рассмотрение примитивов синхронизации с них, а затем перейдем к семафорам.
4.7.3. Пример применения блокировки
Блокировки, как и следовало ожидать, имеют два состояния: заблокированное и разблокированное. Блокировки поддерживают только две функции: acquire и release. Эти функции действуют в полном соответствии с их именами — захват и освобождение.
Иногда необходимость пройти критический участок кода возникает в нескольких потоках. В таком случае можно организовать конкуренцию между потоками за бло- кировку, и первый поток, который сможет ее захватить, получит разрешение войти в критический участок и выполнить содержащийся в нем код. Все остальные одно- временно поступающие потоки блокируются до того времени, когда первый поток завершит свою работу, выйдет из критического участка и освободит блокировку.
06_ch04.indd 213 22.01.2015 22:00:45