Программирование на Python 3. Руководство издательство СимволПлюс
![]()
|
482 Глава 9. Процессы и потоки finally: self.work_queue.task_done() Различия заключаются в том, что в этой версии у нас имеется больше совместно используемых данных и наша функция process() вызывает ся с другими аргументами. Нам не требуется беспокоиться об органи зации доступа к очередям, так как они гарантируют упорядочение доступа; при обращении к другим данным, в данном случае – к слова рю md5_from_filename, мы должны управлять доступом, используя бло кировку. Мы сделали блокировку атрибутом класса, потому что нам требуется, чтобы все экземпляры класса Worker использовали одну и ту же блокировку, то есть когда один экземпляр получает блокировку, все остальные экземпляры при попытке получить ее блокируются. Теперь рассмотрим функцию process(), разделив ее на две части: def process(self, size, filenames): md5s = collections.defaultdict(set) for filename in filenames: with self.Md5_lock: md5 = self.md5_from_filename.get(filename, None) if md5 is not None: md5s[md5].add(filename) else: try: md5 = hashlib.md5() with open(filename, "rb") as fh: md5.update(fh.read()) md5 = md5.digest() md5s[md5].add(filename) with self.Md5_lock: self.md5_from_filename[filename] = md5 except EnvironmentError: continue Сначала создается пустой словарь со значениями по умолчанию, в ко тором каждый ключ является значением контрольной суммы MD5, а значение – множеством имен файлов, которые имеют соответствую щее значение контрольной суммы. Затем выполняются итерации по всем файлам и для каждого файла извлекается его контрольная сумма MD5, если она уже была вычислена ранее; в противном случае она вы числяется. Независимо от того, получаем ли мы доступ к словарю md5_from_filename для чтения или для записи, мы делаем это в контексте блокировки. Экземпляры класса thread ing.Lock() являются менеджерами контекста, которые приобретают блокировку на входе и освобождают ее на выходе. Инструкции with блокируются, пока блокиров ка Md5_Lock не будет освобождена, если какойлибо дру гой поток уже приобрел ее. В первой инструкции with, Менеджеры контекста, стр. 428 Делегирование работы потокам выполнения 483 после приобретения блокировки, из словаря извлекается значение контрольной суммы MD5 (или None, если она еще не вычислялась для текущего файла). Если в качестве контрольной суммы получено значе ние None, ее необходимо вычислить, и в этом случае она записывается в словарь md5_from_filename, чтобы предотвратить повторное ее вычис ление. Обратите внимание, что каждый раз мы стараемся минимизировать объем работы, выполняемой в контексте блокировки, чтобы свести продолжительность блокировки остальных потоков к минимуму – в данном случае в контексте блокировки выполняется единственное обращение к словарю. Строго говоря, мы вообще можем не использовать бло кировки при использовании интерпретатора CPython, так как глобальная блокировка интерпретатора (GIL) эффективно синхронизирует все попытки обращения к словарю. Однако при разработке программы мы реши ли не полагаться на реализацию интерпретатора с гло бальной блокировкой и поэтому явно используем блоки ровку. for filenames in md5s.values(): if len(filenames) == 1: continue self.results_queue.put("{0}Duplicate files ({1:n} bytes):" "\n\t{2}".format(self.number, size, "\n\t".join(sorted(filenames)))) В конце выполняется цикл по элементам локального словаря md5s и для каждого множества, содержащего более одного имени файла, в очередь с результатами добавляется многострочный текст. Добавляе мый текст содержит номер рабочего потока (по умолчанию пустая строка), размер файла в байтах и все имена файлов дубликатов. Нам не требуется использовать блокировку при обращении к очереди с ре зультатами, так как она является экземпляром класса queue.Queue, ко торый автоматически выполняет блокировку. Классы из модуля queue существенно упрощают создание многопоточ ных приложений, а на случай, когда нам требуется использовать бло кировки явно, модуль threading может предложить множество воз можных вариантов. В данном примере мы использовали простейшую блокировку типа threading.Lock, но имеются и другие разновидности блокировок, включая threading.RLock (блокировка, которая может по лучаться повторно потоком, который уже владеет ею), threading.Sema phore (блокировка, которая может использоваться для защиты задан ного числа ресурсов) и threading.Condition, которая обеспечивает усло вие ожидания. Глобальная блокировка GIL, стр. 478 484 Глава 9. Процессы и потоки При использовании нескольких потоков выполнения часто можно получать более простые решения, чем при использовании модуля subprocess, но, к сожалению, мно гопоточные программы на языке Python не всегда позво ляют добиться производительности, какую можно полу чить при реализации обработки данных несколькими процессами. Как уже отмечалось ранее, камнем пре ткновения здесь становится стандартная реализация ин терпретатора Python, поскольку интерпретатор CPy thon в каждый конкретный момент времени может вы полнять программный код на языке Python только на одном процессоре, даже когда в программе выполняется сразу несколько потоков. Попытка решить эту проблему была предпринята в модуле multipro cessing , и, как уже говорилось ранее, среди примеров к этой книге имеется программа grepwordm.py, использующая этот модуль, кото рая отличается от рассматривавшейся выше версии всего тремя стро ками. Похожее преобразование можно выполнить и в программе find duplicatest.py , рассматривавшейся в этом подразделе, но на практике применять этот модуль не рекомендуется. Несмотря на то, что модуль multiprocessing для упрощения подобных преобразований предлагает API (Application Programming Interface – прикладной программный интерфейс), близко совпадающий с API модуля threading, тем не менее эти два API не являются идентичными и имеют некоторые различия. Кроме того, чисто механический переход с использования модуля threading на использование модуля multiprocessing скорее всего воз можен только для небольших и простых программ, таких как grep wordt.py , поэтому лучше разрабатывать программы, изначально опи раясь на модуль multiprocessing. (В примерах к книге имеется про грамма findduplicatesm.py, она выполняет ту же работу, что и про грамма findduplicatest.py, но делает это несколько иначе и использует модуль multiprocessing.) В процессе разработки находится еще одно решение – версия интер претатора CPython с поддержкой выполнения многопоточных про грамм. Самые свежие сведения о ходе разработки можно получить по адресу www.code.google.com/p/pythonthreadsafe. В заключение В этой главе было показано, как создавать программы, запускающие другие программы с помощью модуля subprocess, входящего в состав стандартной библиотеки. Программам, запускаемым с помощью моду ля subprocess, можно передавать аргументы командной строки, постав лять данные через их стандартный поток ввода и получать результаты через их стандартный поток вывода (и через стандартный поток выво да сообщений об ошибках). Возможность создавать дочерние процессы Глобальная блокировка GIL, стр. 478 В заключение 485 позволяет получить максимальную выгоду от наличия многоядерных процессоров и перекладывать работу по обеспечению параллельного выполнения нескольких процессов на плечи операционной системы. Недостаток такого подхода состоит в том, что при необходимости в не скольких процессах совместно использовать некоторые данные или синхронизировать их работу необходимо конструировать некоторый механизм взаимодействия, например, на основе разделяемой памяти (использованием модуля mmap), разделяемых файлов или на основе се тевых взаимодействий, что совершенно очевидно влечет за собой до полнительные хлопоты. В этой главе также было показано, как создавать многопоточные про граммы. К сожалению, такие программы не имеют возможности ис пользовать все преимущества наличия многоядерного процессора (ес ли они работают под управлением стандартной реализации интерпре татора CPython), поэтому для языка Python использование несколь ких процессов часто является более практичным решением, если дело касается производительности. Тем не менее мы видели, что модуль queue и механизмы блокировок языка Python, такие как thread ing.Lock , делают создание многопоточных программ простым делом, и что в случае простых программ, где достаточно применения объек тов модуля queue, таких как queue.Queue и queue.PriorityQueue, можно вообще отказаться от явного использования блокировок. Несмотря на то, что многопоточное программирование несомненно приобретает все большее распространение, тем не менее многопоточ ные программы доставляют больше хлопот при разработке, отладке и сопровождении, чем однопоточные. Однако в многопоточных про граммах проще организовать взаимодействие между потоками, напри мер, посредством совместно используемых данных (при помощи клас сов из модуля queue или с использованием блокировок) и синхрониза цию потоков (например, для сбора результатов), чем в случае несколь ких процессов. Наличие нескольких потоков выполнения может оказаться весьма полезным в программах с графическим интерфей сом, которые должны производить длительные вычисления и сохра нять возможность отклика на действия пользователя, включая воз можность отменить выполнение задачи. Но в случае использования удобного механизма взаимодействий между процессами, такого как разделяемая память или очередь, доступная нескольким процессам, предлагаемая пакетом multiprocessing, использование нескольких про цессов может оказаться более предпочтительной альтернативой мно гопоточным программам. В следующей главе будет продемонстрирован еще один пример много поточной программы сервера, которая обрабатывает каждый запрос клиента в отдельном потоке и использует блокировки для защиты со вместно используемых данных. 486 Глава 9. Процессы и потоки Упражнения 1. Скопируйте и модифицируйте программу grepwordp.py так, чтобы дочерние процессы в ней ничего не выводили, а главная программа собирала бы результаты и после завершения всех дочерних процес сов сортировала и выводила полученные данные. Для этого доста точно будет изменить только функцию main(), добавив три строки и изменив три строки. Для реализации этого упражнения придется проявить внимание и смекалку, и, возможно, вам потребуется про читать документацию с описанием модуля subprocess. Решение при водится в файле grepwordp_ans.py. 2. Напишите многопоточную программу, которая читает содержимое файлов, имена которых перечислены в командной строке (и рекур сивно – содержимое файлов во всех каталогах, перечисленных в ко мандной строке). Все файлы, которые являются файлами XML (то есть начинающиеся с символов «кальных тегов, используемых в файле, или вывести сообщение, ес ли возникла какаялибо ошибка. Ниже приводится пример вывода программы: ./data/dvds.xml is an XML file that uses the following tags: dvd dvds ./data/bad.aix is an XML file that has the following error: mismatched tag: line 7889, column 2 ./data/incidents.aix is an XML file that uses the following tags: airport incident incidents narrative Самый простой способ написать такую программу состоит в том, чтобы модифицировать копию программы findduplicatest.py, хотя вы, конечно, можете написать программу с самого начала. Неболь шие изменения придется внести в метод __init__() и __run__() клас са Worker, а метод process() придется переписать полностью (но для этого потребуется всего около двадцати строк). В функцию main() программы потребуется внести несколько упрощений, а функция print_results() будет выводить однострочный текст. Сообщение о порядке использования также потребуется изменить, чтобы оно выглядело, как показано ниже: Usage: xmlsummary.py [options] [path] outputs a summary of the XML files in path; path defaults to . Options: h, help show this help message and exit t COUNT, threads=COUNT Упражнения 487 the number of threads to use (1..20) [default 7] v, verbose d, debug (Порядок использования: xmlsummary.py [параметры] [путь] выводит сведения о файлах XML в пути path; по умолчанию используется путь . Параметры: h, help показать это сообщение и выйти t COUNT, threads=COUNT Число используемых потоков (1..20) [по умолчанию 7] v, verbose d, debug ) Обязательно попробуйте запустить программу в отладочном режи ме, чтобы проверить, как запускаются потоки и что каждый из них выполняет свою часть работы. Решение приводится в файле xml summary.py , в котором чуть больше 100 строк и не используются явные блокировки. 10 Сети Сети позволяют компьютерным программам взаимодействовать друг с другом, даже если они выполняются на разных машинах. Для одних программ, таких как вебброузеры, работа в сети является основным видом их деятельности, тогда как для других работа в сети является всего лишь дополнением к их функциональным возможностям – на пример, выполнение удаленных операций и регистрация событий или получение и передача данных другим машинам. Большинство сетевых программ работают либо по схеме точкаточка (когда одна и та же про грамма выполняется на разных машинах), либо по более общей схеме клиент/сервер (программыклиенты отправляют запросы серверу). В этой главе мы создадим простое приложение, работающее по схеме клиент/сервер. Такие приложения обычно состоят из двух отдельных программ: программы сервера, ожидающей поступления запросов и от вечающей на них, и одного или более клиентов, которые отправляют запросы серверу и получают от него ответы. Для обеспечения нормаль ной работы клиентов необходимо знать, как подключиться к серверу, то есть необходимо знать IPадрес сервера и номер порта. 1 Кроме того, и клиент и сервер должны передавать и принимать данные в понятных им форматах. Низкоуровневый модуль socket (на котором основаны все высокоуров невые сетевые модули в языке Python) поддерживает как адреса IPv4, так и адреса IPv6. Он также поддерживает наиболее широко используе мые сетевые протоколы, включая UDP (User Datagram Protocol – про 1 Существует также возможность выполнять подключения с помощью меха низма обнаружения служб, например, с использованием Bonjour API – со ответствующие модули можно найти в каталоге пакетов Python Package In dex, pypi.python.org/pypi. • Создание клиента TCP • Создание сервера TCP Клиент TCP 489 токол пользовательских дейтаграмм) – легковесный, но ненадежный протокол, не обладающий поддержкой постоянных соединений, вы полняющий передачу данных пакетами (дейтаграммами), но не гаран тирующий их доставку адресату, и TCP (Transmission Control Proto col – протокол управления передачей) – надежный протокол, поддер живающий установку постоянных соединений и ориентированный на потоковый режим передачи данных. С помощью протокола TCP мож но передавать и принимать любые объемы данных – сокет отвечает за деление данных на пакеты достаточно маленького размера, чтобы их можно было отправлять по сети, а также за их восстановление на дру гом конце соединения. Протокол UDP часто используется в инструментах мониторинга, кото рые обеспечивают непрерывное чтение поступающих данных и для ко торых потеря некоторых пакетов не является существенной. Иногда этот протокол используется для передачи потокового аудио или видео, когда потеря отдельных кадров считается вполне допустимым явлени ем. Протоколы FTP и HTTP построены на базе протокола TCP, и обыч но приложения, работающие в схеме клиент/сервер, используют про токол TCP, потому что им необходимы постоянные соединения и на дежность, которые обеспечиваются протоколом TCP. В этой главе мы будем разрабатывать приложение, работающее в схеме клиент/сервер, поэтому мы будем использовать протокол TCP. Еще нам необходимо определиться с тем, как будут вы полняться отправка и получение данных – в виде тексто вых строк или в виде блоков двоичных данных, а во вто ром случае – в каком формате. В этой главе мы будем ис пользовать блоки двоичных данных, где первые четыре байта (кодированные как целые числа с использованием модуля struct) будут определять длину последующего блока данных в двоичном формате – в виде законсерви рованных объектов, которые воспроизводятся модулем pickle . Преимущество такого подхода состоит в том, что мы можем использовать один и тот же программный код для передачи и приема в любых приложениях, посколь ку в виде законсервированных объектов можно сохра нять практически любые данные. Недостаток такого подхода заключается в том, что и клиент и сервер долж ны уметь работать с законсервированными объектами, поэтому обе программы должны быть написаны на язы ке Python или обладать доступом к интерпретатору Py thon, например, Jython в Java или Boost.Python в C++. И, конечно, при использовании модуля pickle не следует забывать о проблеме безопасности. Пример, который мы будем рассматривать, представляет собой про грамму регистрации автомобиля. На сервере хранится вся регистраци Модуль pickle , стр. 341 490 Глава 10. Сети онная информация (номерной знак, количество посадочных мест, про бег и владелец). Программаклиент будет использоваться для получе ния информации об автомобиле, позволяя изменять величину пробега или владельца, а также создавать новые регистрационные записи. Од новременно может использоваться любое число клиентских программ, и они не будут блокировать друг друга, даже если два клиента одно временно обратятся к серверу. Это обеспечивается тем, что сервер бу дет обслуживать каждого клиента в отдельном потоке. (Мы также уви дим, насколько просто можно организовать обработку клиентов в от дельных процессах.) Исключительно ради демонстрации мы будем запускать сервер и кли ентов на одной и той же машине. Это означает, что будет использовать ся «локальный» IPадрес (хотя, если сервер выполняется на другой машине, клиенту можно передать ее IPадрес в виде аргумента ко мандной строки и они смогут взаимодействовать друг с другом при ус ловии, что этому не будут препятствовать системы сетевой защиты). Кроме того, мы произвольно выбрали номер порта 9653. Номер порта должен быть больше 1023, и желательно, чтобы он находился в диапа зоне от 5001 до 32787, хотя вполне допустимыми считаются номера портов вплоть до 65535. Сервер может получать пять типов запросов: GET_CAR_DETAILS, CHANGE_ MILEAGE , CHANGE_OWNER, NEW_REGISTRATION и SHUTDOWN и отправлять соответ ствующие ответы для каждого из них. В ответ передаются запрошен ные данные, подтверждение выполнения запрошенного действия или признак ошибки. |