справочник по Python. мм isbn 9785932861578 9 785932 861578
Скачать 4.21 Mb.
|
578 Глава 21. Работа с сетью и сокеты экземпляр класса SystemCall. Этот объект называется «системным вызо- вом», потому что он имитирует способ обращения к системным службам в настоящих многозадачных операционных системах, таких как UNIX или Windows. В частности, когда программе требуется обратиться к службе операционной системы, она передает управление системному вызову и предоставляет некоторую дополнительную информацию, не- обходимую для выполнения операции. В этом смысле возврат объекта класса SystemCall напоминает вызов системной «ловушки». • Объект класса Scheduler представляет коллекцию объектов класса Task, которыми он управляет. Вся основная работа диспетчера построена во- круг очереди задач (атрибут task_queue), в которой хранятся задачи, го- товые к запуску. Над очередью задач выполняются четыре основных операции. Метод new() принимает новую задачу, заворачивает ее в объ- ект класса Task и помещает созданный объект в очередь. Метод sched- ule() получает существующий объект класса Task и вставляет его об- ратно в очередь. Метод mainloop() запускает цикл диспетчера, который обрабатывает задачи одну за другой, пока в очереди не останется задач. Методы readwait() и writewait() помещают объект класса Task во времен- ную область ожидания, где он остается до появления ожидаемого собы- тия ввода-вывода. В этом случае работа задачи приостанавливается, но она не уничтожается, а просто бездействует в ожидании. • Метод mainloop() является основой диспетчера задач. В самом начале этот метод проверяет, имеются ли задачи, ожидающие событий ввода- вывода. Если такие задачи имеются, диспетчер подготавливает и вы- зывает функцию select(), чтобы проверить наличие событий ввода- вывода. Если имеются какие-либо события, представляющие интерес, соответствующие задачи возвращаются обратно в очередь задач, чтобы появилась возможность запустить их. Затем метод mainloop() вытал- кивает очередную задачу из очереди и вызывает ее метод run(). Если какая-либо задача завершает работу (возбуждает исключение StopItera- tion ), она уничтожается. Если задача просто возвращает управление, она опять добавляется в очередь задач, чтобы обеспечить возможность ее запуска в следующем цикле. Так продолжается до тех пор, пока либо не опустеет очередь задач, либо все задачи не окажутся приостановлен- ными в ожидании событий ввода-вывода. Метод mainloop() может при- нимать дополнительный аргумент count, позволяющий обеспечить за- вершение работы метода после указанного количества операций опро- са. Это может пригодиться, когда диспетчер встраивается в другой цикл обработки событий. • Самым сложным аспектом диспетчера является обработка экземпля- ров класса SystemCall в методе mainloop(). Когда задача возвращает эк- земпляр класса SystemCall, диспетчер вызывает его метод handle(), пе- редавая в качестве аргументов соответствующие экземпляры классов Scheduler и Task. Назначение системного вызова состоит в том, чтобы выполнить некоторую внутреннюю операцию, необходимую задаче или диспетчеру. Классы ReadWait(), WriteWait() и NewTask() являются примера- ми системных вызовов, которые приостанавливают выполнение задачи на время операции ввода-вывода или создают новую задачу. Например, Модуль select 579 конструктор ReadWait() принимает задачу и вызывает метод readwait() диспетчера. После этого диспетчер принимает задачу и помещает ее в соответствующую область ожидания. Здесь можно видеть соблюде- ние очень важного принципа разделения объектов. Задачи возвращают объекты класса SystemCall службе обработки запросов, но они не вступа- ют в прямое взаимодействие с диспетчером. Объекты класса SystemCall, в свою очередь, могут выполнять операции над задачами или диспетче- рами, но они никак не привязаны к конкретной реализации диспетчера или задачи. Благодаря этому теоретически имеется возможность созда- вать совершенно разные реализации диспетчера (возможно даже с ис- пользование потоков управления), которые могли бы просто вставлять- ся в существующий фреймворк и он при этом продолжал бы работать. Ниже приводится пример простого сетевого сервера времени, реализован- ного с помощью данного диспетчера задач. Он поможет понять многое из того, о чем говорилось в предыдущем списке: from socket import socket, AF_INET, SOCK_STREAM def time_server(address): import time s = socket(AF_INET,SOCK_STREAM) s.bind(address) s.listen(5) while True: yield ReadWait(s) conn,addr = s.accept() print(“Получен запрос на соединение с %s” % str(addr)) yield WriteWait(conn) resp = time.ctime() + “\r\n” conn.send(resp.encode(‘latin-1’)) conn.close() ёё sched = Scheduler() sched.new(time_server((‘’,10000))) # Сервер на порту 10000 sched.new(time_server((‘’,11000))) # Сервер на порту 11000 sched.run() В этом примере параллельно запускаются два сервера – каждый из них использует свой номер порта для приема соединений (это легко проверить с помощью утилиты telnet). Инструкции yield ReadWait() и yield WriteWait() вызывают приостановку сопрограммы каждого из серверов до момента, пока не появится возможность выполнить операцию ввода-вывода над со- ответствующим сокетом. Когда эти инструкции возвращают управление сопрограмме, немедленно выполняется соответствующая операция ввода- вывода, такая как accept() или send(). Конструкции ReadWait и WriteWait могут показаться слишком низкоуровне- выми. К счастью, архитектура приложения позволяет скрыть эти опера- ции за кулисами библиотечных функций и методов, при условии, что они также будут являться сопрограммами. Взгляните на следующий объект, который служит оберткой вокруг объекта и имитирует его интерфейс: class CoSocket(object): def __init__(self,sock): 580 Глава 21. Работа с сетью и сокеты self.sock = sock def close(self): yield self.sock.close() def bind(self,addr): yield self.sock.bind(addr) def listen(self,backlog): yield self.sock.listen(backlog) def connect(self,addr): yield WriteWait(self.sock) yield self.sock.connect(addr) def accept(self): yield ReadWait(self.sock) conn, addr = self.sock.accept() yield CoSocket(conn), addr def send(self,bytes): while bytes: evt = yield WriteWait(self.sock) nsent = self.sock.send(bytes) bytes = bytes[nsent:] def recv(self,maxsize): yield ReadWait(self.sock) yield self.sock.recv(maxsize) Н иже приводится реализация сервера времени, основанная на примене- нии класса CoSocket: from socket import socket, AF_INET, SOCK_STREAM def time_server(address): import time s = CoSocket(socket(AF_INET,SOCK_STREAM)) yield s.bind(address) yield s.listen(5) while True: conn,addr = yield s.accept() print(conn) print(“Получен запрос на соединение с %s” % str(addr)) resp = time.ctime()+”\r\n” yield conn.send(resp.encode(‘latin-1’)) yield conn.close() ёё sched = Scheduler() sched.new(time_server((‘’,10000))) # Сервер на порту 10000 sched.new(time_server((‘’,11000))) # Сервер на порту 11000 sched.run() В этом примере программный интерфейс объекта класса CoSocket выгля- дит, как интерфейс обычного сокета. Единственное отличие состоит в том, что каждая операция должна предваряться инструкцией yield (так как все методы определены, как сопрограммы). На первый взгляд все это вы- глядит так странно, что заставляет задаться вопросом: а есть ли в этом какой-то смысл? Если запустить сервер, который приводится выше, можно заметить, что одновременно может выполняться несколько его копий без применения потоков управления или дочерних процессов. При этом про- Модуль select 581 грамма выглядит, как «обычный» поток управления, если игнорировать все инструкции yield. Ниже приводится пример асинхронного веб-сервера, который одновремен- но обслуживает несколько соединений с клиентами, но не использует при этом функции обратного вызова, потоки управления или процессы. Срав- ните его с примерами, реализованными на основе применения модулей asynchat и asyncore. import os import mimetypes try: from http.client import responses # Python 3 except ImportError: from httplib import responses # Python 2 from socket import * ёё def http_server(address): s = CoSocket(socket(AF_INET,SOCK_STREAM)) yield s.bind(address) yield s.listen(50) ёё while True: conn,addr = yield s.accept() yield NewTask(http_request(conn,addr)) del conn, addr ёё def http_request(conn,addr): request = b”” while True: data = yield conn.recv(8192) request += data if b’\r\n\r\n’ in request: break ёё header_data = request[:request.find(b’\r\n\r\n’)] header_text = header_data.decode(‘latin-1’) header_lines = header_text.splitlines() method, url, proto = header_lines[0].split() if method == ‘GET’: if os.path.exists(url[1:]): yield serve_file(conn,url[1:]) else: yield error_response(conn,404,”File %s not found” % url) else: yield error_response(conn,501,”%s method not implemented” % method) yield conn.close() ёё def serve_file(conn,filename): content,encoding = mimetypes.guess_type(filename) yield conn.send(b”HTTP/1.0 200 OK\r\n”) yield conn.send((“Content-type: %s\r\n” % content).encode(‘latin-1’)) yield conn.send((“Content-length: %d\r\n” % os.path.getsize(filename)).encode(‘latin-1’)) yield conn.send(b”\r\n”) f = open(filename,”rb”) 582 Глава 21. Работа с сетью и сокеты while True: data = f.read(8192) if not data: break yield conn.send(data) ёё def error_response(conn,code,message): yield conn.send((“HTTP/1.0 %d %s\r\n” % (code, responses[code])).encode(‘latin-1’)) yield conn.send(b”Content-type: text/plain\r\n”) yield conn.send(b”\r\n”) yield conn.send(message.encode(‘latin-1’)) ёё sched = Scheduler() sched.new(http_server((‘’,8080))) sched.mainloop() Внимательное изучение этого примера позволит надежно усвоить особен- ности приемов разработки многозадачных программ с применением сопро- грамм, которые используются некоторыми весьма сложными сторонними модулями. Однако чрезмерное употребление этих приемов может привести к тому, что вас уволят с работы после очередной инспекции вашего про- граммного кода. Когда имеет смысл использовать асинхронные операции при работе с сетью Использование асинхронных операций ввода-вывода (модули asyncore и asynchat), операций опроса и сопрограмм, как это демонстрировалось в предыдущих примерах, остается одним из самых таинственных аспектов программирования на языке Python. И все же эти приемы используются намного чаще, чем можно было бы подумать. Одной из часто упоминаемых причин использования асинхронных операций ввода-вывода является ми- нимизация нагрузки, связанной с большим количеством потоков управ- ления, особенно когда возникает необходимость управлять множеством клиентов при наличии ограничений, связанных с глобальной блокировкой интерпретатора (см. главу 20 «Потоки и многозадачность»). Исторически модуль asyncore был одним из первых модулей в стандарт- ной библиотеке, обеспечившим поддержку асинхронного ввода-вывода. Модуль asynchat появился немного позже с целью упростить использо- вание модуля asyncore. Оба эти модуля используют подход, основанный на обработке событий ввода-вывода. Например, когда возникает событие ввода-вывода, вызывается функция обратного вызова. Функция обратного вызова реагирует на событие ввода-вывода и выполняет некоторую обра- ботку данных. Если вам придется создавать крупные приложения в таком стиле, вы обнаружите, что обработка событий пронизывает практически все части приложения (например, события ввода-вывода приводят к вы- зову функций-обработчиков, которые в свою очередь вызывают другие функции-обработчики, и так до бесконечности). Этот подход используется в пакете Twisted (http://twistedmatrix.com), одном из наиболее популярных пакетов, предназначенных для разработки сетевых приложений. Модуль select 583 Б олее современные решения опираются на применение сопрограмм, но они сложнее и используются реже, так как сопрограммы впервые появились в версии Python 2.5. Одна из важнейших особенностей сопрограмм состо- Python 2.5. Одна из важнейших особенностей сопрограмм состо- 2.5. Одна из важнейших особенностей сопрограмм состо- ит в том, что они позволяют писать приложения, которые больше походят на многопоточные программы. Например, в примерах реализации веб- сервера не используются функции обратного вызова, и они выглядят очень похожими на программы, основанные на использовании потоков управле- ния, – нужно просто привыкнуть к использованию инструкции yield. В ин- терпретаторе Python, не использующем стек вызовов (http://www.stackless. com ), эта идея развита еще дальше. Вообще говоря, в большинстве сетевых приложений не следует стремиться использовать приемы асинхронного ввода-вывода. Например, если потре- буется создать сервер, который постоянно отправляет данные через сотни или даже тысячи соединений одновременно, применение методики, осно- ванной на создании множества потоков управления, может обеспечить бо- лее высокую производительность. Это обусловлено тем, что производитель- ность функции select() снижается тем больше, чем больше число соедине- ний, которые нужно отслеживать. В операционной системе Linux этот не- Linux этот не- этот не- достаток можно устранить за счет использования специальных функций, таких как epoll(), но это ограничивает переносимость программного кода. Пожалуй, самую большую выгоду от использования асинхронного ввода- вывода можно получить в приложениях, где сетевые операции необходимо интегрировать в другие циклы событий (например, в цикл событий гра- фического интерфейса) или где сетевые операции приходится добавлять в программный код, который выполняет массивные вычисления. В подоб- ных ситуациях использование асинхронного ввода-вывода способно умень- шить время отклика. Исключительно в целях демонстрации ниже приводится программа, вы- полняющая задачу, о которой поется в песне «10 миллионов бутылок пива на стене»: bottles = 10000000 ёё def drink_beer(): remaining = 12.0 while remaining > 0.0: remaining -= 0.1 ёё def drink_bottles(): global bottles while bottles > 0: drink_beer() bottles -= 1 Теперь предположим, что необходимо добавить возможность удаленного мониторинга, которая позволяла бы клиентам подключаться и следить за тем, сколько бутылок осталось. Один из вариантов состоит в том, чтобы за- пустить сервер в отдельном потоке управления, заставив его выполняться параллельно с основным приложением, как показано ниже: 584 Глава 21. Работа с сетью и сокеты def server(port): s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.bind((‘’,port)) s.listen(5) while True: client,addr = s.accept() client.send((“%d bottles\r\n” % bottles).encode(‘latin-1’) client.close() ёё # Запуск сервера мониторинга thr = threading.Thread(target=server,args=(10000,)) thr.daemon=True thr.start() drink_bottles() Другой вариант состоит в том, чтобы реализовать сервер на основе опроса каналов ввода-вывода и внедрить операцию опроса непосредственно в глав- ный цикл вычислений. Ниже приводится пример, в котором используется диспетчер сопрограмм, разработанный выше: def drink_bottles(): global bottles while bottles > 0: drink_beer() bottles -= 1 scheduler.mainloop(count=1,timeout=0) # Проверить наличие соединений ёё # Асинхронный сервер, основанный на сопрограммах. def server(port): s = CoSocket(socket.socket(socket.AF_INET,socket.SOCK_STREAM)) yield s.bind((‘’,port)) yield s.listen(5) while True: client,addr = yield s.accept() yield client.send((“%d bottles\r\n” % bottles).encode(‘latin-1’) yield client.close() ёё scheduler = Scheduler() scheduler.new(server(10000)) drink_bottles() Если написать отдельную программу, которая периодически будет вы- полнять подключение к программе, имитирующей опустошение бутылок пива, и измерять время, необходимое на получение информации о коли- честве оставшихся бутылок, результаты могут оказаться весьма удиви- тельными. На компьютере автора (макбук с двухъядерным процессором, работающим на частоте 2 ГГц) среднее время отклика сервера (измерения проводились по 1000 запросов), основанного на сопрограмме, составило примерно 1 миллисекунду, против 5 миллисекунд для сервера, основанно- го на потоках управления. Такая разница объясняется тем, что реализа- ция, основанная на сопрограмме, способна отвечать сразу же, как только обнаруживает попытку установить соединение, тогда как многопоточный сервер не может быть запущен, пока не будет запланирован на выполнение Модуль select 585 операционной системой. Учитывая наличие потока управления, выполня- ющего массивные вычисления, и глобальной блокировки интерпретатора, сервер вынужден простаивать, пока вычислительный поток управления не исчерпает выделенный ему квант времени. Во многих системах вели- чина кванта времени составляет примерно 10 миллисекунд, то есть вы- шеупомянутое время отклика многопоточного сервера точно соответствует среднему арифметическому значению времени ожидания переключения вычислительного потока операционной системой. Недостатком периодического опроса является чрезмерная нагрузка, если его выполнять слишком часто. Например, хотя время отклика в примере с опросом оказалось меньше, общее время выполнения программы увели- чилось более чем на 50%. Если изменить реализацию так, что опрос будет проводиться только через каждые шесть бутылок пива, время отклика увеличится весьма незначительно и составит 1,2 миллисекунды, тогда как время выполнения программы будет всего на 3% больше, чем время выпол- нения программы без опроса. К сожалению, часто не бывает иного способа четко определить, как часто следует выполнять опрос, кроме как выпол- нив измерение производительности приложения. Несмотря на то что уменьшение времени отклика кажется победой, реа- лизация собственного механизма многозадачности может породить не- приятные проблемы. Например, в задачах необходимо с особой осторож- ностью подходить к выполнению любых операций, которые могут вызы- вать при остановку процесса. В примере с веб-сервером имеется фрагмент программного кода, который открывает файл и читает из него данные. Эта операция может приостановить выполнение всей программы на доста- точно продолжительный промежуток времени, если доступ к файлу будет сопряжен с необходимостью перемещения головок жесткого диска. Един- ственный способ устранить эту проблему состоит в том, чтобы дополни- тельно реализовать асинхронный доступ к файлу и добавить эту особен- ность в диспетчер задач. Для более сложных операций, таких как выпол- нение запросов к базе данных, определить, как реализовать асинхронный доступ, может оказаться намного сложнее. Один из возможных способов реализации таких операций – вынести их в отдельный поток управления и организовать обмен результатами с диспетчером задач по мере их посту- пления, что можно осуществить с помощью очередей сообщений. В некото- рых системах существуют низкоуровневые системные вызовы, выполняю- щие асинхронные операции ввода-вывода (например, семейство функций aio_* в UNIX). К моменту написания этих строк в стандартной библиотеке языка Python еще не был реализован доступ к этим функциям, однако вы можете попробовать поискать сторонние модули, обеспечивающие такую возможность. По опыту автора, использование подобных функциональных возможностей намного сложнее, чем выглядит, а выигрыш от их добавле- ния в программу на языке Python не стоит затрачиваемых усилий – часто в таких ситуациях более предпочтительно бывает использовать библиоте- ку для работы с потоками управления. |