Только что описанная модель ветвления в целом хорошо работает на Unix-подобных платформах, но потенциально страдает существенными ограничениями:
Производительность
На некоторых компьютерах запуск нового процесса обходится довольно дорого в отношении ресурсов времени и памяти.
Переносимость
Ветвление процессов — это инструмент Unix. Как мы уже знаем, функция os.fork в настоящее время не работает на платформах, отличных от Unix, таких как Windows под управлением стандартной версии Python. Как мы также узнали ранее, функцию fork можно использовать в Windows, в версии Python для Cygwin, но она может быть недостаточно эффективной и не точно соответствовать версии fork в Unix. И, как мы только что обнаружили, модуль multiprocessing не способен решить проблему в Windows, потому что подключенные сокеты не могут передаваться в сериализованном виде через границы процессов.
Сложность
Если вам кажется, что организация серверов на основе ветвления процессов может оказаться сложной, то вы правы. Как мы только что видели, ветвление приводит ко всей этой мороке по управлению и удалению зомби, — к зачистке после дочерних процессов, завершающихся раньше, чем их родители.
После прочтения главы 5 вам должно быть известно, что обычным решением этих проблем является использование по то ков вы пол не ния вместо процессов. Потоки выполняются параллельно и совместно используют глобальную память (то есть память модуля и интерпретатора).
Поскольку все потоки выполняются в пределах одного процесса и в той же области памяти, они автоматически получают сокеты в общее пользование и могут передавать их друг другу, примерно так же, как дочерние процессы получают в наследство дескрипторы сокетов. Однако, в отличие от процессов, запуск потоков обычно требует меньших издержек, а работать в настоящее время они могут и в Unix-подобных системах, и в Windows, под управлением стандартных версий Python. Кроме того, многие (хотя и не все) считают потоки более простыми в программировании — дочерние потоки завершаются тихо, не оставляя за собой зомби, преследующих сервер.
В примере 12.7 представлена еще одна версия эхо-сервера, в которой параллельная обработка клиентских запросов выполняется в потоках, а не в процессах.
Пример 12.7. PP4E\Internet\Sockets\thread-server.py
На стороне сервера: открывает сокет с указанным номером порта, ожидает появления сообщения от клиента и отправляет это же сообщение обратно; продолжает возвращать сообщения клиенту, пока не будет получен признак eof при закрытии сокета на стороне клиента; для обслуживания клиентов порождает дочерние потоки выполнения; потоки используют глобальную память совместно с главным потоком; этот прием является более переносимым, чем ветвление: потоки выполнения действуют в стандартных версиях Python для Windows, тогда как прием ветвления — нет;
import time, _thread as thread # или использовать threading.Thread().start() from socket import * # получить конструктор сокетов и константы
myHost = » # компьютер-сервер, » означает локальный хост
myPort = 50007 # использовать незарезервированный номер порта
sockobj = socket(AF_INET, SOCK_STREAM) # создать объект сокета TCP sockobj.bind((myHost, myPort)) # связать с номером порта сервера
sockobj.listen(5) # не более 5 ожидающих запросов
def now():
return time.ctime(time.time())
def handleClient(connection): # в дочернем потоке: ответить
time.sleep(5) # имитировать блокирующие действия
while True: # чтение, запись в сокет клиента
data = connection.recv(1024) if not data: break reply = ‘Echo=>%s at %s’ % (data, now()) connection.send(reply.encode()) connection.close()
def dispatcher(): # пока процесс работает,
while True: # ждать запроса очередного клиента,
connection, address = sockobj.accept() # передать потоку
print(‘Server connected by’, address, end=’ ‘) # для обслуживания print(‘at’, now()) thread.start_new_thread(handleClient, (connection,))
dispatcher()
Эта функция dispatcher передает каждый входящий клиентский запрос в новый порождаемый поток, выполняющий функцию handleClient. Благодаря этому данный сервер может одновременно обрабатывать несколько клиентов, а главный цикл диспетчера может быстро вернуться в начало и проверить поступление новых запросов. В результате новым клиентам не будет отказано в обслуживании из-за занятости сервера.
Функционально эта версия аналогична решению на основе функции fork (клиенты обрабатываются параллельно), но может работать в любой системе, поддерживающей потоки выполнения, в том числе в Windows и Linux. Проверим ее работу в обеих системах. Сначала запустим сервер в Linux, а клиентские сценарии — в Linux и в Windows:
[окно 1: серверный процесс, использующий потоки; сервер продолжает принимать запросы клиентов и при этом обслуживание предыдущих запросов производится в дочерних потоках]
[…]$ python thread-server.py
Server connected by (‘127.0.0.1’, 37335) at Sun Apr 25 08:59:05 2010
Server connected by (‘72.236.109.185’, 58866) at Sun Apr 25 08:59:54 2010
Server connected by (‘72.236.109.185’, 58867) at Sun Apr 25 08:59:56 2010
Server connected by (‘72.236.109.185’, 58868) at Sun Apr 25 08:59:58 2010
[окно 2: клиент, выполняющийся на компьютере сервера] […]$ python echo—client.py
Client received: b"Echo=>b’Hello network world’ at Sun Apr 25 08:59:10 2010"
[окна 3-5: локальные клиенты, ПК]
C:\…\PP4E\Internet\Sockets> python echo-client.py learning-python.com Client received: b"Echo=>b’Hello network world’ at Sun Apr 25 08:59:59 2010"
C:\…\PP4E\Internet\Sockets> python echo-clie nt.py learning-python.com Bruce
Client received: b"Echo=>b’Bruce’ at Sun Apr 25 09:00:01 2010"
C:\…\Sockets> python echo-client.py learning-python.com The Meaning
of life
Client received: b"Echo=>b’The’ at Sun Apr 25 09:00:03 2010"
Client received: b"Echo=>b’Meaning’ at Sun Apr 25 09:00:03 2010"
Client received: b"Echo=>b’of’ at Sun Apr 25 09:00:03 2010"
Client received: b"Echo=>b’life’ at Sun Apr 25 09:00:03 2010"
Поскольку вместо ветвящихся процессов этот сервер использует потоки выполнения, его можно запускать переносимым образом в Linux и в Windows. Снова запустим его, на этот раз на одном и том же локальном компьютере с Windows, вместе с клиентами. Главным, на что следует обратить внимание, здесь является то, что во время обслуживания предшествующих клиентов новые запросы могут приниматься и обслуживаться параллельно с другими клиентами и главным потоком (во время 5-секундной задержки):
[окно 1: сервер на локальном PC]
C:\…\PP4E\Internet\Sockets> python thread-server.py
Server connected by (‘127.0.0.1’, 58987) at Sun Apr 25 12:41:46 2010
Server connected by (‘127.0.0.1’, 58988) at Sun Apr 25 12:41:47 2010
Server connected by (‘127.0.0.1’, 58989) at Sun Apr 25 12:41:49 2010
[окна 2-4: клиенты на локальном PC]
C:\…\PP4E\Internet\Sockets> python echo-client.py
Client received: b"Echo=>b’Hello network world’ at Sun Apr 25 12:41:51 2010"
C:\…\PP4E\Internet\Sockets> python
Client received: b"Echo=>b’Brian’ at
C:\…\PP4E\Internet\Sockets> python echo-client.py localhost Bright side of life
Client received: b"Echo=>b’Bright’ at Sun Apr 25 12:41:54 2010"
Client received: b"Echo=>b’side’ at Sun Apr 25 12:41:54 2010"
Client received: b"Echo=>b’of’ at Sun Apr 25 12:41:54 2010"
Client received: b"Echo=>b’life’ at Sun Apr 25 12:41:54 2010"
Напомню, что поток просто завершается при возврате из функции, которую он выполняет, — в отличие от версии с ветвлением процессов, в функции обслуживания клиента не вызывается ничего похожего на os._exit (этого и нельзя делать — можно завершить все потоки в процессе!). Благодаря этому версия с потоками не только более переносима, но и проще.
Использованная литература:
Марк Лутц — Программирование на Python, 4-е издание, II том, 2011