Многопоточное программированиеВ этой главе
Скачать 0.74 Mb.
|
Перенос приложения в версию 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 Глава 4 Многопоточное программирование 214 С этого момента возможность захватить блокировку и войти в критический участок получает любой из оставшихся ожидающих потоков. Заслуживает внимания то, что отсутствует какое-либо упорядочение потоков, работа которых организована с помо- щью блокировок (т.е. применяется принцип простой очереди — “первым пришел, первым обслуживается”); процесс выбора потока-победителя не детерминирован и может зависеть даже от применяемой реализации Python. Рассмотрим, с чем связана необходимость применения блокировок. Сценарий mtsleepF.py представляет собой приложение, в котором происходит порождение случайным образом выбранного количества потоков, в каждом из которых осущест- вляется выход после завершения работы. Рассмотрим следующий базовый фрагмент исходного кода (для версии Python 2): from atexit import register from random import randrange from threading import Thread, currentThread from time import sleep, ctime class CleanOutputSet(set): def __str__(self): return ', '.join(x for x in self) loops = (randrange(2,5) for x in xrange(randrange(3,7))) remaining = CleanOutputSet() def loop(nsec): myname = currentThread().name remaining.add(myname) print '[%s] Started %s' % (ctime(), myname) sleep(nsec) remaining.remove(myname) print '[%s] Completed %s (%d secs)' % ( ctime(), myname, nsec) print ' (remaining: %s)' % (remaining or 'NONE') def _main(): for pause in loops: Thread(target=loop, args=(pause,)).start() @register def _atexit(): print 'all DONE at:', ctime() Более подробное построчное описание кода мы приведем вслед за окончательным вариантом сценария, в котором применяются блокировки, но вкратце можно отме- тить, что сценарий mtsleepF.py по сути лишь дополняет приведенные ранее приме- ры. Как и в примере сценария bookrank.py, немного упростим код. Для этого отло- жим на время применение средств объектно-ориентированного программирования, исключим список объектов потока и операции join() с потоками и снова введем в действие метод atexit.register() (по тем же причинам, как и в коде bookrank.py). Проведем еще одно небольшое изменение по отношению к приведенным ранее примерам mtsleepX.py. Вместо жесткого задания пары операций приостановки ци- клов/потоков на 4 и 2 секунды соответственно, внесем некую неопределенность, созда- вая случайным образом от 3 до 6 потоков, каждый из которых может приостанавли- ваться на какой-то промежуток времени от 2 до 4 секунд. 06_ch04.indd 214 22.01.2015 22:00:46 215 4.7. Практическое применение многопоточной обработки В этом сценарии применяются также некоторые новые средства, причем наиболее заметным среди них является использование множества для хранения имен остав- шихся потоков, которые все еще функционируют. Причина, по которой создается подкласс объекта множества вместо непосредственного использования самого класса, состоит в том, что это позволяет продемонстрировать еще один вариант использова- ния множества, в котором изменяется применяемое по умолчанию для вывода стро- ковое представление множества. При использовании операции вывода содержимого множества формируются примерно такие результаты: set([X, Y, Z,...]). Однако это не очень удобно, по- скольку потенциальные пользователи нашего приложения не знают (и не должны знать) о том, что такое множества и для чего они используются в программе. Таким образом, необходимо вместо этого вывести данные, которые выглядят примерно как X, Y, Z, .... Именно по этой причине мы создали подкласс класса set и реализова- ли его метод __str__(). После этого изменения, при условии, что вся остальная часть сценария будет ра- ботать правильно, должен сформироваться аккуратный вывод, который будет иметь подходящее выравнивание: $ python mtsleepF.py [Sat Apr 2 11:37:26 2011] Started Thread-1 [Sat Apr 2 11:37:26 2011] Started Thread-2 [Sat Apr 2 11:37:26 2011] Started Thread-3 [Sat Apr 2 11:37:29 2011] Completed Thread-2 (3 secs) (remaining: Thread-3, Thread-1) [Sat Apr 2 11:37:30 2011] Completed Thread-1 (4 secs) (remaining: Thread-3) [Sat Apr 2 11:37:30 2011] Completed Thread-3 (4 secs) (remaining: NONE) all DONE at: Sat Apr 2 11:37:30 2011 However, if you're unlucky, you might get strange output such as this pair of example executions: $ python mtsleepF.py [Sat Apr 2 11:37:09 2011] Started Thread-1 [Sat Apr 2 11:37:09 2011] Started Thread-2 [Sat Apr 2 11:37:09 2011] Started Thread-3 [Sat Apr 2 11:37:12 2011] Completed Thread-1 (3 secs) [Sat Apr 2 11:37:12 2011] Completed Thread-2 (3 secs) (remaining: Thread-3) (remaining: Thread-3) [Sat Apr 2 11:37:12 2011] Completed Thread-3 (3 secs) (remaining: NONE) all DONE at: Sat Apr 2 11:37:12 2011 $ python mtsleepF.py [Sat Apr 2 11:37:56 2011] Started Thread-1 [Sat Apr 2 11:37:56 2011] Started Thread-2 [Sat Apr 2 11:37:56 2011] Started Thread-3 [Sat Apr 2 11:37:56 2011] Started Thread-4 [Sat Apr 2 11:37:58 2011] Completed Thread-2 (2 secs) [Sat Apr 2 11:37:58 2011] Completed Thread-4 (2 secs) (remaining: Thread-3, Thread-1) (remaining: Thread-3, Thread-1) [Sat Apr 2 11:38:00 2011] Completed Thread-1 (4 secs) 06_ch04.indd 215 22.01.2015 22:00:46 Глава 4 Многопоточное программирование 216 (remaining: Thread-3) [Sat Apr 2 11:38:00 2011] Completed Thread-3 (4 secs) (remaining: NONE) all DONE at: Sat Apr 2 11:38:00 2011 Что же произошло? Во-первых, очевидно, что результаты далеко не однородны (поскольку возможность выполнять операции ввода-вывода параллельно предостав- лена сразу нескольким потокам). Подобное чередование результирующих данных можно было также наблюдать и в некоторых примерах приведенного ранее кода. Во-вторых, обнаруживается такая проблема, что два потока изменяют значение од- ной и той же переменной (множества, содержащего имена оставшихся потоков). Операции ввода-вывода и операции доступа к одной и той же структуре данных входят в состав критических разделов кода, поэтому необходимы блокировки, ко- торые смогли бы воспрепятствовать одновременному вхождению в эти разделы не- скольких потоков. Чтобы ввести в действие блокировки, необходимо добавить стро- ку кода для импорта объекта Lock (или RLock), создать объект блокировки и внести дополнения или изменения в код, позволяющие применять блокировки в нужных местах: from threading import Thread, Lock, currentThread lock = Lock() Теперь необходимо обеспечить установку и снятие блокировки. В следующем коде показаны вызовы acquire() и release(), которые должны быть введены в функцию loop(): def loop(nsec): myname = currentThread().name lock.acquire() remaining.add(myname) print '[%s] Started %s' % (ctime(), myname) lock.release() sleep(nsec) lock.acquire() remaining.remove(myname) print '[%s] Completed %s (%d secs)' % ( ctime(), myname, nsec) print ' (remaining: %s)' % (remaining or 'NONE') lock.release() После внесения изменений полученный вывод уже не должен содержать прежних искажений: $ python mtsleepF.py [Sun Apr 3 23:16:59 2011] Started Thread-1 [Sun Apr 3 23:16:59 2011] Started Thread-2 [Sun Apr 3 23:16:59 2011] Started Thread-3 [Sun Apr 3 23:16:59 2011] Started Thread-4 [Sun Apr 3 23:17:01 2011] Completed Thread-3 (2 secs) (remaining: Thread-4, Thread-2, Thread-1) [Sun Apr 3 23:17:01 2011] Completed Thread-4 (2 secs) (remaining: Thread-2, Thread-1) 06_ch04.indd 216 22.01.2015 22:00:46 217 4.7. Практическое применение многопоточной обработки [Sun Apr 3 23:17:02 2011] Completed Thread-1 (3 secs) (remaining: Thread-2) [Sun Apr 3 23:17:03 2011] Completed Thread-2 (4 secs) (remaining: NONE) all DONE at: Sun Apr 3 23:17:03 2011 Исправленная (и окончательная) версия mtsleepF.py показана в примере 4.10. |