Современное асинхронное программирование сокетов: Свой асинхронный tcp-сервер за 15 минут с подробным разбором

Содержание

Свой асинхронный tcp-сервер за 15 минут с подробным разбором


Ранее я представил пару небольших постов о потенциальной роли Spring Boot 2 в реактивном программировании. После этого я получил ряд вопросов о том, как работают асинхронные операции в программировании в целом. Сегодня я хочу разобрать, что такое Non-blocking I/O и как применить это знание для создания небольшого tcp–сервера на python, который сможет обрабатывать множество открытых и тяжелых (долгих) соединений в один поток. Знание python не требуется: все будет предельно просто со множеством комментариев. Приглашаю всех желающих!

Мне, как и многим другим разработчикам, очень нравятся эксперименты, поэтому вся последующая статья будет состоять как раз из серии экспериментов и выводов, которые они несут. Предполагается, что вы недостаточно хорошо знакомы с тематикой, и будете охотно экспериментировать со мной. Исходники примеров можно найти на github.

Начнем с написания очень простого tcp–сервера.

Задача сервера будет заключаться в получении и печати данных из сокета и возвращения строки Hello from server!. Примерно так это выглядит:

Синхронный tcp-сервер
import socket

# Задаем адрес сервера
SERVER_ADDRESS = ('localhost', 8686)

# Настраиваем сокет
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(SERVER_ADDRESS)
server_socket.listen(10)
print('server is running, please, press ctrl+c to stop')

# Слушаем запросы
while True:
    connection, address = server_socket.accept()
    print("new connection from {address}".format(address=address))

    data = connection.recv(1024)
    print(str(data))

    connection.send(bytes('Hello from server!', encoding='UTF-8'))

    connection.close()


Здесь все довольно просто. Если вы не знакомы с понятием сокета, то вот очень простая и практическая статья. Мы создаем сокет, ловим входящие соединения и обрабатываем их согласно заданной логике. Здесь стоит обратить внимание на сообщения. При создании нового соединения с клиентом мы пишем об этом в консоль.

Хочу сразу заметить, что не стоит серьезно вникать в листинги программ до полного прочтения статьи. Совершенно нормально, если что-то будет не совсем понятно в самом начале. Просто продолжайте чтение.

Нет особого смысла в сервере без клиента. Поэтому на следующем этапе необходимо написать небольшой клиент для использования сервера:

tcp-клиент
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import socket

MAX_CONNECTIONS = 20
address_to_server = ('localhost', 8686)

clients = [socket.socket(socket.AF_INET, socket.SOCK_STREAM) for i in range(MAX_CONNECTIONS)]
for client in clients:
    client.connect(address_to_server)

for i in range(MAX_CONNECTIONS):
    clients[i].send(bytes("hello from client number " + str(i), encoding='UTF-8'))

for client in clients:
    data = client.recv(1024)
    print(str(data))


Важной особенностью здесь является тот факт, что мы в начале устанавливаем максимально возможное количество соединений, а только потом используем их для передачи/хранения данных.

Давайте запустим сервер. Первое, что мы видим:

server is running, please, press ctrl+c to stop

Это означает, что мы успешно запустили наш сервер и он готов принимать входящие запросы. Запустим клиент и сразу увидим в консоли сервера (у вас порты могут быть другими):
server is running, please, press ctrl+c to stop
new connection from ('127.0.0.1', 39196)
b'hello from client number 0'
new connection from ('127.0.0.1', 39198)
b'hello from client number 1'
...

Что и следовало ожидать. В бесконечном цикле мы получаем новое соединение и сразу же обрабатываем данные из него. В чем тут проблема? Ранее мы использовали опцию server_socket.listen(10) для настройки сервера. Она означает максимальный размер очереди из еще не принятых подключений. Но в этом нет совершенно никакого смысла, ведь мы принимаем по одному соединению. Что предпринять в такой ситуации? На самом деле, существует несколько выходов.
  1. Распараллелить с помощью потоков/процессов (для этого можно, например, использовать fork или пул).
    Подробнее здесь.
  2. Обрабатывать запросы не по мере их подключения к серверу, а по мере наполнения этих соединений нужным количеством данных. Проще говоря, мы можем открыть сразу максимальное количество ресурсов и читать из них столько, сколько сможем (сколько необходимо на это процессорного времени в идеальном случае).

Вторая идея кажется заманчивой. Всего один поток и обработка множества соединений. Давайте посмотрим, как это будет выглядеть. Не стоит пугаться обилия кода. Если что-то сразу не понятно, то это вполне нормально. Можно попробовать запустить у себя и подебажить:Асинхронный сервер

import select
import socket

SERVER_ADDRESS = ('localhost', 8686)

# Говорит о том, сколько дескрипторов единовременно могут быть открыты
MAX_CONNECTIONS = 10

# Откуда и куда записывать информацию
INPUTS = list()
OUTPUTS = list()


def get_non_blocking_server_socket():

    # Создаем сокет, который работает без блокирования основного потока
    server = socket.
socket(socket.AF_INET, socket.SOCK_STREAM) server.setblocking(0) # Биндим сервер на нужный адрес и порт server.bind(SERVER_ADDRESS) # Установка максимального количество подключений server.listen(MAX_CONNECTIONS) return server def handle_readables(readables, server): """ Обработка появления событий на входах """ for resource in readables: # Если событие исходит от серверного сокета, то мы получаем новое подключение if resource is server: connection, client_address = resource.accept() connection.setblocking(0) INPUTS.append(connection) print("new connection from {address}".format(address=client_address)) # Если событие исходит не от серверного сокета, но сработало прерывание на наполнение входного буффера else: data = "" try: data = resource.recv(1024) # Если сокет был закрыт на другой стороне except ConnectionResetError: pass if data: # Вывод полученных данных на консоль print("getting data: {data}".
format(data=str(data))) # Говорим о том, что мы будем еще и писать в данный сокет if resource not in OUTPUTS: OUTPUTS.append(resource) # Если данных нет, но событие сработало, то ОС нам отправляет флаг о полном прочтении ресурса и его закрытии else: # Очищаем данные о ресурсе и закрываем дескриптор clear_resource(resource) def clear_resource(resource): """ Метод очистки ресурсов использования сокета """ if resource in OUTPUTS: OUTPUTS.remove(resource) if resource in INPUTS: INPUTS.remove(resource) resource.close() print('closing connection ' + str(resource)) def handle_writables(writables): # Данное событие возникает когда в буффере на запись освобождается место for resource in writables: try: resource.send(bytes('Hello from server!', encoding='UTF-8')) except OSError: clear_resource(resource) if __name__ == '__main__': # Создаем серверный сокет без блокирования основного потока в ожидании подключения server_socket = get_non_blocking_server_socket() INPUTS.
append(server_socket) print("server is running, please, press ctrl+c to stop") try: while INPUTS: readables, writables, exceptional = select.select(INPUTS, OUTPUTS, INPUTS) handle_readables(readables, server_socket) handle_writables(writables) except KeyboardInterrupt: clear_resource(server_socket) print("Server stopped! Thank you for using!")


Давайте запустим наш новый сервер и посмотрим на консоль:
Вывод асинхронного сервера
server is running, please, press ctrl+c to stop
new connection from ('127.0.0.1', 56608)
new connection from ('127.0.0.1', 56610)
new connection from ('127.0.0.1', 56612)
new connection from ('127.0.0.1', 56614)
new connection from ('127.0.0.1', 56616)
new connection from ('127.0.0.1', 56618)
new connection from ('127.0.0.1', 56620)
new connection from ('127.0.0.1', 56622)
new connection from ('127.0.0.1', 56624)
getting data: b'hello from client number 0'
new connection from ('127.
0.0.1', 56626) getting data: b'hello from client number 1' getting data: b'hello from client number 2'


Как можно понять по выводу, мы принимаем новые коннекты и данные почти параллельно. Более того, мы не ждем данных от нового подключения. Вместо этого мы устанавливаем новое.
Как это работает?

Дело в том, что все наши операции с ресурсами (а обращение к сокету относится к этой категории) происходят через системные вызовы операционной системы. Если говорить коротко, то системные вызовы представляют собой обращение к API операционной системы.

Рассмотрим, что происходит в первом случае и во втором.

Синхронный вызов

Давайте разберем рисунок:

Первая стрелка показывает, что наше приложение обращается к операционной системе для получения данных из ресурса. Далее наша программа блокируется до появления нужного события. Минус очевиден: если у нас один поток, то другие пользователи должны ждать обработку текущего.

Асинхронный вызов

Теперь посмотрим на рисунок, который иллюстрирует асинхронный вызов:

Первая стрелка, как и в первом случае, делает запрос к ОС (операционной системе) на получение данных из ресурсов. Но посмотрите, что происходит далее. Мы не ждем данных из ресурса и продолжаем работу. Что же делать нам? Мы отдали распоряжение ОС и не ждем результат сразу. Простейшим ответом будет самостоятельно опрашивать нашу систему на наличие данных. Таким образом, мы сможем утилизировать ресурсы и не блокировать наш поток.

Но самом деле такая система не является практичной. Такое состояние, в котором мы постоянно смотрим на данные и ждем какого-то события, называется активным ожиданием. Минус очевиден: мы впустую тратим процессорное время в случае, когда информация не обновилась. Более правильным решением было бы оставить блокировку, но сделать ее «умной». Наш поток не просто ждет какого-то определенного события. Вместо этого он ожидает любое изменение данных в нашей программе. Именно так и работает функция select в нашем асинхронном сервере:

Теперь можно вернуться к реализации нашего асинхронного сервера и взглянуть на него с новым знанием. Первое, что бросается в глаза, – метод работы. Если в первом случае наша программа выполнялась “сверху вниз”, то во втором случае мы оперируем событиями. Такой подход в разработке ПО называется событийно-ориентированным (event-driven development).

Сразу стоит отметить, что такой подход не является серебряной пулей. У него имеется масса недостатков. Во-первых, такой код сложнее поддерживать и менять. Во-вторых, у нас есть и всегда будут блокирующие вызовы, которые все портят. Например, в программе выше мы воспользовались функцией print. Дело в том, что такая функция тоже обращается к ОС, следовательно, наш поток выполнения блокируется и другие источники данных терпеливо ждут.

Заключение


Выбор подхода напрямую зависит от решаемой нами задачи. Позвольте задаче самой выбрать наиболее продуктивный подход. Например, популярный Javaвеб -сервер Tomcat использует потоки. Не менее популярный сервер Nginx использует асинхронный подход. Создатели популярного веб-сервера gunicorn на python пошли по пути prefork.

Спасибо, что прочитали статью до конца! В следующий раз (уже скоро) я расскажу о прочих возможных неблокирующих ситуациях в жизни наших программ. Буду рад вас видеть и в следующих постах.

Разбираемся с устройством асинхронных фреймворков для Python — «Хакер»

Содержание статьи

Асинхронные приложения — это типичный пример того, про что говорят «Новое — это хорошо забытое старое». Ну да, сам по себе подход появился еще очень давно, когда надо было эмулировать параллельное выполнение задач на одноядерных процессорах и старых архитектурах. Но песок — плохая замена овсу, «асинхронность» и «параллельность» — довольно-таки ортогональные понятия, и один подход задачи другого не решает. Тем не менее асинхронности нашлось отличное применение в наше высоконагруженное время быстрых интернет-сервисов с тысячами и сотнями тысяч клиентов, ждущих обслуживания одновременно. Возможно, стоит разобраться получше, как это все работает?

 

Зачем нужна асинхронность?

Если спуститься почти на самый низ, к ядру операционной системы, то у программиста, откровенно говоря, есть только два варианта работы с сокетом — синхронный и асинхронный.

С синхронным в целом все понятно — пришел клиент, открылся сокет, передали данные, если это все — сокет закрылся. В этом случае пока мы не закончили локальный диалог с одним клиентом — не можем начать его с другим. По такому принципу обычно работают простые серверы, которым не надо держать сотни и тысячи клиентов. В случае если нагрузка возрастает, но не критично — можно создать еще один или несколько потоков (или даже процессов) и обрабатывать подключения еще и в них. Это обкатанный годами, стабильно работающий подход, который, например, использует сервер Apache, — никаких неожиданностей, данные от клиентов обрабатываются в порядке строгой очереди, а в случае запуска какого-то «долгого» кода — например, каких-то вычислений или хитрого запроса в БД — это все никак не влияет на других клиентов.

Но есть проблема: сервер — это не Лернейская гидра, он не может плодить потоки и процессы вечно — есть же, в конце концов, вполне ощутимые ресурсы, которые тратятся при каждом таком действии, и имеется верхний порог использования этих ресурсов. И вот тогда все вдруг вспомнили про асинхронность и системные вызовы для неблокирующего ввода-вывода. Зачем плодить кучу сокетов и потоков, выедать ресурсы, если можно данные от многих клиентов сразу одновременно слушать на одном сокете?

 

Все началось с системных вызовов

Собственно, вариантов системных вызовов для неблокирующей работы с сетевым вводом-выводом не так уж и много (хотя они слегка и разнятся от платформы к платформе). Самый первый, базовый, можно сказать ветеран — это системный вызов select(), который появился еще в бородатые восьмидесятые годы вместе с первой версией того, что сейчас называется POSIX-сокетами (то есть сокетами в понимании большинства современных серверных систем), а тогда называлось Berkeley sockets, сокетами Беркли.

По большому счету, во времена описания системного вызова select() вообще мало кто задумывался о том, что когда-то приложения могут стать НАСТОЛЬКО высоконагруженными. Фактически все, что этот вызов умеет делать, — принимать фиксированное количество (по умолчанию не более 1024) однозначно описанных в программе файловых дескрипторов и слушать события на них. При готовности дескриптора к чтению или записи запустится соответствующий колбэк-метод в коде.

Почему select() все еще существует и используется?

Ну, во-первых, он существует именно потому, что существует, как бы каламбурно это ни звучало, — select() поддерживается практически всеми мыслимыми и немыслимыми программными платформами, которые вообще подразумевают сетевое взаимодействие. А во-вторых, есть, скажем так, «городская легенда», что в силу простой, как топор, реализации этот системный вызов на части архитектур (к которым не относятся ни широко используемые персональные компьютеры, ни даже серверы) обладает феноменальной точностью обработки тайм-аутов (вплоть до наносекунд). Возможно, при работе в области космических исследований или ядерной энергетики это спасет чью-то жизнь? Кто знает.

Потом кто-то задумался о том, что неплохо бы все-таки научиться делать действительно по-взрослому высоконагруженные сетевые приложения, и появился системный вызов poll(). Кстати, в Linux он существует довольно давно, а вот в Windows его не было до выпуска Windows Vista. Вместо разрозненных сокетов этот вызов принимает на вход структуру со списком дескрипторов (фактически произвольного размера, без ограничений) и возможных событий на них. Затем система начинает в цикле бегать по этой структуре и отлавливать события.

Главный минус вызова poll() (хотя это, несомненно, был большой шаг вперед по сравнению с select()) — обход структуры с дескрипторами с точки зрения алгоритмики линеен, то есть осуществляется за O(n). Причем это касается не только отслеживания событий, но и реакции на них, да еще и надо передавать информацию туда-обратно из kernel space в user space.

А вот дальше в каждой операционной системе решили пойти своим путем. Нельзя сказать, что подходы конкретно различаются, но все-таки реализовать кросс-платформенную асинхронную работу с сокетами в своей программе стало чуточку сложнее. Под Windows появился API работы с так называемыми IO Completion Ports, в BSD-системах добавили механизм kqueue/kevent, а в Linux, начиная с ядра 2.5. 44, стал работать системный вызов epoll. Отлов асинхронных событий на сокетах (как бы тавтологично это ни звучало) стал асинхронным сам по себе, то есть вместо обхода структур операционная система умеет подавать сигнал о событии в программу практически моментально после того, как это событие произошло. Кроме того, сокеты для мониторинга стало можно добавлять и убирать в любой момент времени. Это и есть те самые технологии, которые сегодня широко используются в большинстве сетевых фреймворков.

 

Зоопарк event loop’ов

Основная идея программирования с использованием вышеописанных штук состоит в том, что на уровне приложения реализуется так называемый event loop, то есть цикл, в котором непосредственно происходит отлов событий и дергаются callback’и. Под *nix-системами давненько уже существуют обертки, которые позволяют максимально упростить работу с сокетом и абстрагировать написанный код от низкоуровневой системной логики. Например, существует известная библиотека libevent, а также ее младшая сестра libev. Эти библиотеки собираются под разные системы и позволяют использовать самый совершенный из доступных механизмов мониторинга событий.

Я буду приводить в пример большей частью пакеты для сетевого программирования на языке Python, ибо их действительно там целый зоопарк на любой вкус, а еще они популярны и широко используются в различных проектах. Даже в самом языке довольно давно уже существуют встроенные модули asyncore и asynchat, которые, хоть и не умеют работать с epoll (только select/poll), вполне подходят для написания своих реализаций протоколов.

Одна из проблем сетевых библиотек заключается в том, что в каждой из них написана своя имплементация event loop’а, поэтому, даже несмотря на общий подход, перенос, скажем, плагина для Twisted (Reactor) на Tornado (IOLoop) или наоборот может оказаться вовсе не тривиальной задачей. Эту проблему призван решить новый встроенный модуль в Python 3.4, который называется asyncio и, вопреки расхожему мнению, не является сетевой библиотекой или веб-фреймворком в полном смысле слова, а является именно что встроенной в язык реализацией event loop’а. Эта штука как раз и призвана сплотить сторонние библиотеки вокруг одной общей стабильной технологии. Если хочется немного подробностей и независимых впечатлений об asyncio — милости прошу сюда.

Для Tornado уже существует реализация поддержки event loop’а из asyncio, и, более того, она не так давно вышла из состояния беты. Посмотреть можно здесь. Для Twisted релиз asyncio тоже не оказался неожиданностью, и его разработчики даже написали своеобразный шутливый некролог для проекта, в котором, напротив, уверяют, что это вовсе не конец, а очень даже начало новой эпохи развития.
Если говорить уж совсем откровенно, то понятие асинхронного ввода-вывода необязательно должно относиться именно к сетевому сокету. Системы семейства *nix следуют принципу, согласно которому взаимодействие фактически с любым устройством или сервисом происходит через file-like объект. Примерами таких объектов могут служить UNIX-сокеты или, скажем, ноды псевдофайловой системы /dev, через которые осуществляется обмен информацией с блочными устройствами. Соответственно, говоря об event loop’ах, мы можем подразумевать не только сетевое взаимодействие, но и просто любой асинхронный I/O. А чтобы было что потрогать руками — советую глянуть, например, вот на этот встроенный модуль из Python 3.4.

 

Запутанная история

В современном мире фреймворк Twisted выглядит таким своеобразным мамонтом, legacy-архаизмом, который впитал в себя все попытки предоставить удобный интерфейс для написания сетевых приложений. Тем не менее интерфейс получился действительно удобный, с реализацией отложенного выполнения кода и прочими плюшками, когда никакого Node.js еще не существовало и в помине.

Как я уже упомянул выше, реализация event loop’а в Twisted называется Reactor. Суть работы с ним состоит в том, что мы регистрируем callback’и, которые выполняются в глобальном цикле в виде реакции на какие-то события. Выглядеть это может, например, так:

from twisted.internet import reactor
class Countdown(object):
counter = 5
def count(self):
if self. counter == 0:
reactor.stop()
else:
print(self.counter)
self.counter -= 1
reactor.callLater(1, self.count) # регистрируем callback
# Передаем реактору метод, который нужно дернуть на старте
reactor.callWhenRunning(Countdown().count)
print('Start!')
reactor.run() # поехали!
print('Stop!’)
"""
Start!
5
4
3
2
1
Stop!
"""

Причем, даже если внутри callback возникнет исключение, то реактор все равно продолжит работать, после того как залогирует трейсбэк и запустит зарегистрированные обработчики ошибок.

Кстати, нельзя не уточнить, что Twisted по умолчанию однопоточный, то есть вся эта развесистая петрушка реализована на внутренней магии вокруг системных вызовов для асинхронного I/O. Но на случай крайней нужды в нем есть и своя реализация ThreadPool, которая добавляет возможность работы нескольких потоков.

 

Сюда идет Tornado

Если Twisted все-таки представляет собой больше сетевую библиотеку, чем веб-фреймворк, то c Tornado все ровно наоборот. Этот пакет был разработан теми же товарищами, которые делали FriendFeed, — а это, на минуточку, реально высоконагруженный веб-проект с миллионами посетителей в день. Отличие выражается еще и в том, что в нем есть небольшой встроенный шаблонизатор и поддержка всяких «интернетных» технологий вроде работы с куками и генератора HTTP-ответов в разных форматах.
Кстати, Tornado появился немного раньше, чем в Python появилась встроенная поддержка epoll (обертки вокруг сетевых системных вызовов находятся в модуле select), поэтому он поставляется с небольшой библиотекой для этого вызова, написанной в несколько строчек кода на C в виде импортируемого модуля. Эта обертка используется на старых версиях Python, но, говорят, в некоторых случаях специально руками собранный с ней пакет работает чуточку быстрее, чем на ванильной реализации. Да, еще одна городская легенда.

Twisted возник на горизонте раньше Tornado, однако тогда еще не начался этот бум на асинхронные веб-приложения, а когда он все-таки пришел, то Twisted оказался слегка не у дел в этой сфере, потому что изначально смотрел немного в другую сторону. Это выразилось в том, что веб-приложения на Twisted сейчас в основном пишут только приверженцы старой школы, а для Tornado появилось довольно большое число библиотек, которые добавляют, например, асинхронную работу с базами данных и key-value хранилищами, удобную интеграцию с фронтенд-технологиями наподобие SockJS и SocketIO и все такое прочее. В результате он сейчас является прямым конкурентом Node.js, только из мира Python.

В качестве примера асинхронного подхода рассмотрим такой код:

import time

import tornado.web
from tornado.ioloop import IOLoop
from tornado import gen

@gen.coroutine # Регистрируем в event loop’е метод как корутину
def async_sleep(seconds):
# Да, это вам не обычный sleep(). Мы добавляем таск с тайм-аутом в IOLoop для текущего обработчика
yield gen.Task(IOLoop.instance().add_timeout, time.time() + seconds)

class TestHandler(tornado.web.RequestHandler):
@gen.coroutine
def get(self):
for i in xrange(100):
print i
yield async_sleep(1)
self. write(str(i)) # Асинхронно пишем в сокет
self.finish() # Завершаем составление ответа

application = tornado.web.Application([
(r"/test", TestHandler),
])

application.listen(9999)
IOLoop.instance().start() # Поехали!

Про то, что такое корутины и как они работают, можно прочитать в моей статье в октябрьском номере. Этот код можно считать примером простейшего асинхронного приложения на Tornado — запускается сервер на 9999-м порту, который при заходе по URL «/test» запускает отложенную таску, в которой каждую секунду шлет следующее число из счетчика в сокет, при этом не забывая обрабатывать другие подключения.

 

Освещаем события

Асинхронные серверы — это круто, но как же насчет асинхронных клиентов? Такие тоже писать довольно легко. В Python это можно делать с использованием одной из двух довольно известных библиотек — gevent и eventlet. На их основе создаются отличные скоростные парсеры и системы мониторинга, которые по-настоящему быстро опрашивают тысячи серверов.

Нет, на самом деле серверы с их помощью тоже можно писать. Например, в известной облачной open source платформе OpenStack eventlet используется как база при построении REST-сервисов в некоторых подпроектах. Но в этих библиотеках также присутствует действительно хорошая инфраструктура для написания клиентов.

Gevent работает в основном с библиотекой libevent (или, в новых версиях, libev), а eventlet может при желании работать и просто с epoll. Основная задача этих модулей — создание удобной инфраструктуры для работы с корутинами и запуск тасков в «зеленом» режиме, то есть реализация кооперативной многозадачности за счет быстрого переключения контекста.

Например, в eventlet есть механизм манкипатчинга стандартной библиотеки Python, который позволяет при использовании, скажем, модуля threading, подменять потоки на корутины. То есть в общем случае написанная в многопоточном стиле программа становится асинхронной.

В качестве примера кода с переключением контекста приведу код из примеров стандартной библиотеки gevent:

import gevent

def foo():
print('Running in foo')
gevent. sleep(0)
print('Explicit context switch to foo again')

def bar():
print('Explicit context to bar')
gevent.sleep(0)
print('Implicit context switch back to bar')

gevent.joinall([
gevent.spawn(foo), # Превращаем наши методы в «корутины»
gevent.spawn(bar),
]) # и дожидаемся выполнения

"""
Running in foo
Explicit context to bar
Explicit context switch to foo again
Implicit context switch back to bar
"""

А вот первый же пример простейшего асинхронного клиента на eventlet (его и другие примеры можно найти на официальном сайте):

import eventlet
# Патченная версия модуля из стандартной библиотеки
from eventlet.green import urllib2

urls = [
"https://www.google.com/intl/en_ALL/images/logo.gif",
"http://python.org/images/python-logo.gif",
"http://us.i1.yimg.com/us.yimg.com/i/ww/beta/y3.gif",
]

def fetch(url):
print("opening", url)
body = urllib2. urlopen(url).read()
print("done with", url)
return url, body

pool = eventlet.GreenPool(200) # Пул асинхронных обработчиков
for url, body in pool.imap(fetch, urls):
print("got body from", url, "of length", len(body))

Основной и главной проблемой этих модулей можно назвать то, что в силу их завязанности на код на C и хитрости реализации их до сих пор в нормальном виде не портировали ни на PyPy, ни на Python 3, есть только прототипы.

 

И что в итоге?

С одной стороны, асинхронный подход к программированию крайне полезен при решении целого ряда задач, особенно в сфере веб-разработки. С другой — он зачастую требует вывернуть мозг наизнанку, так как любая непродуманная строчка кода может привести к блокировке всего и вся. Но, несомненно, знание того, как работают подобные вещи, может быть очень полезным для современного разработчика, особенно в свете развития таких языков, как Go и Erlang, которые внутри себя скрещивают сразу несколько видов асинхронности и многопоточности в одном флаконе. Поэтому — категорически рекомендую пробовать, ошибаться, радоваться и вообще программировать. Удачи!

Асинхронное программирование в Python 3: использование Asincio, примеры кода

От автора: в этом руководстве вы познакомитесь с функциями асинхронного ввода-вывода, представленными в Python 3.4 и улучшенными в Python 3.5 и 3.6.

Ранее в Python было мало вариантов для асинхронного программирования. Новая функция асинхронного ввода-вывода, наконец, обеспечивает необходимую поддержку, которая включает в себя как высокоуровневые API, так и стандартную поддержку, нацеленную на объединение нескольких сторонних решений (Twisted, Gevent, Tornado, asyncore и т.д.).

Важно понимать, что изучение асинхронного ввода-вывода Python не является простой задачей из-за скоростной итерации, области действия и необходимости обеспечить способ миграции на существующие асинхронные среды. Я сосредоточусь на последних и самых простых аспектах.

Существует множество вещей, которые интересным образом взаимодействуют через границы потоков, границы процессов и удаленные машины. Существуют отличия и ограничения для конкретной платформы. Давайте разберемся в этом.

Подключаемый цикл событий

Основной концепцией асинхронного ввода-вывода является цикл обработки событий. В программе может быть несколько циклов событий. Каждый поток будет иметь не более одного активного цикла обработки событий. Цикл обработки событий предоставляет следующие возможности:

Бесплатный курс «Python. Быстрый старт»

Получите курс и узнайте, как создать программу для перевода текстов на Python

Получить курс

Регистрация, выполнение и отмена отложенных вызовов (с задержками).

Создание клиентских и серверных транспортов для различных видов связи.

Запуск подпроцессов и связанных транспортов для связи с внешней программой.

Делегирование ресурсозатратных вызовов функций в пул потоков.

Вот простой пример, в котором запускаются две сопрограммы и вызывается функция с задержкой. Он показывает, как использовать цикл обработки событий для работы вашей программы:

import asyncio async def foo(delay): for i in range(10): print(i) await asyncio.sleep(delay) def stopper(loop): loop.stop() loop = asyncio.get_event_loop() # Schedule a call to foo() loop.create_task(foo(0.5)) loop.create_task(foo(1)) loop.call_later(12, stopper, loop) # Block until loop.stop() is called() loop.run_forever() loop.close()

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

import asyncio

async def foo(delay):

    for i in range(10):

        print(i)

        await asyncio.sleep(delay)

def stopper(loop):

    loop.stop()

loop = asyncio. get_event_loop()

# Schedule a call to foo()

loop.create_task(foo(0.5))

loop.create_task(foo(1))

loop.call_later(12, stopper, loop)

# Block until loop.stop() is called()

loop.run_forever()

loop.close()

Класс AbstractEventLoop обеспечивает базовое соглашение для циклов событий. Есть много вещей, которые должен поддерживать цикл обработки событий:

Планирование функций и сопрограмм для выполнения

Создание фьючерсов и задач

Управление TCP-серверами

Обработка сигналов (в Unix)

Работа с пайпами и подпроцессами

Вот методы, связанные с запуском и остановкой события, а также планированием функций и сопрограмм:

class AbstractEventLoop: «»»Abstract event loop.»»» # Running and stopping the event loop. def run_forever(self): «»»Run the event loop until stop() is called.»»» raise NotImplementedError def run_until_complete(self, future): «»»Run the event loop until a Future is done. Return the Future’s result, or raise its exception. «»» raise NotImplementedError def stop(self): «»»Stop the event loop as soon as reasonable. Exactly how soon that is may depend on the implementation, but no more I/O callbacks should be scheduled. «»» raise NotImplementedError def is_running(self): «»»Return whether the event loop is currently running.»»» raise NotImplementedError def is_closed(self): «»»Returns True if the event loop was closed.»»» raise NotImplementedError def close(self): «»»Close the loop. The loop should not be running. This is idempotent and irreversible. No other methods should be called after this one. «»» raise NotImplementedError def shutdown_asyncgens(self): «»»Shutdown all active asynchronous generators.»»» raise NotImplementedError # Methods scheduling callbacks. All these return Handles. def _timer_handle_cancelled(self, handle): «»»Notification that a TimerHandle has been cancelled.»»» raise NotImplementedError def call_soon(self, callback, *args): return self.call_later(0, callback, *args) def call_later(self, delay, callback, *args): raise NotImplementedError def call_at(self, when, callback, *args): raise NotImplementedError def time(self): raise NotImplementedError def create_future(self): raise NotImplementedError # Method scheduling a coroutine object: create a task. def create_task(self, coro): raise NotImplementedError # Methods for interacting with threads. def call_soon_threadsafe(self, callback, *args): raise NotImplementedError def run_in_executor(self, executor, func, *args): raise NotImplementedError def set_default_executor(self, executor): raise NotImplementedError

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

class AbstractEventLoop:

    «»»Abstract event loop. «»»

    # Running and stopping the event loop.

    def run_forever(self):

        «»»Run the event loop until stop() is called.»»»

        raise NotImplementedError

    def run_until_complete(self, future):

        «»»Run the event loop until a Future is done.

        Return the Future’s result, or raise its exception.

        «»»

        raise NotImplementedError

    def stop(self):

        «»»Stop the event loop as soon as reasonable.

        Exactly how soon that is may depend on the implementation, but

        no more I/O callbacks should be scheduled.

        «»»

        raise NotImplementedError

    def is_running(self):

        «»»Return whether the event loop is currently running.»»»

        raise NotImplementedError

    def is_closed(self):

        «»»Returns True if the event loop was closed.»»»

        raise NotImplementedError

    def close(self):

        «»»Close the loop.

        The loop should not be running.

        This is idempotent and irreversible.

        No other methods should be called after this one.

        «»»

        raise NotImplementedError

    def shutdown_asyncgens(self):

        «»»Shutdown all active asynchronous generators.»»»

        raise NotImplementedError

    # Methods scheduling callbacks.  All these return Handles.

    def _timer_handle_cancelled(self, handle):

        «»»Notification that a TimerHandle has been cancelled.»»»

        raise NotImplementedError

    def call_soon(self, callback, *args):

        return self.call_later(0, callback, *args)

    def call_later(self, delay, callback, *args):

        raise NotImplementedError

    def call_at(self, when, callback, *args):

        raise NotImplementedError

    def time(self):

        raise NotImplementedError

    def create_future(self):

        raise NotImplementedError

    # Method scheduling a coroutine object: create a task.

    def create_task(self, coro):

        raise NotImplementedError

    # Methods for interacting with threads.

    def call_soon_threadsafe(self, callback, *args):

        raise NotImplementedError

    def run_in_executor(self, executor, func, *args):

        raise NotImplementedError

    def set_default_executor(self, executor):

        raise NotImplementedError

Подключение нового цикла событий

Asyncio предназначен для поддержки нескольких реализаций циклов событий, которые придерживаются его API. Ключ — это класс EventLoopPolicy, который настраивает asyncio и позволяет контролировать каждый аспект цикла событий. Вот пример пользовательского цикла обработки событий uvloop , основанного на libuv, который должен быть намного быстрее, чем его альтернативы (я не тестировал его сам):

import asyncio import uvloop asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())

import asyncio

import uvloop

asyncio. set_event_loop_policy(uvloop.EventLoopPolicy())

Вот и все. Теперь, когда вы используете любую функцию asyncio, она запускает uvloop под капотом.

Сопрограммы, фьючерсы и задачи

Сопрограмма — это составной термин. Это функция, которая выполняется асинхронно, и объект, который необходимо запланировать. Вы определяете их, добавляя ключевое слово async перед определением:

import asyncio async def cool_coroutine(): return «So cool…»

import asyncio

async def cool_coroutine():

    return «So cool…»

Если вы вызовите такую функцию, она не запустится. Вместо этого она возвращает объект сопрограммы, и если вы не запланируете его выполнение, вы также получите предупреждение:

c = cool_coroutine() print(c) Output: <coroutine object cool_coroutine at 0x108a862b0> sys:1: RuntimeWarning: coroutine ‘cool_coroutine’ was never awaited Process finished with exit code 0

c = cool_coroutine()

print(c)

Output:

<coroutine object cool_coroutine at 0x108a862b0>

sys:1: RuntimeWarning: coroutine ‘cool_coroutine’ was never awaited

Process finished with exit code 0

Чтобы на самом деле выполнить сопрограмму, нам нужен цикл обработки событий:

r = loop. run_until_complete(c) loop.close() print(r) Output: So cool…

r = loop.run_until_complete(c)

loop.close()

print(r)

Output:

So cool…

Это прямое планирование. Вы также можете поставить в очередь сопрограммы. Обратите внимание, что при вызове сопрограмм вы должны вызывать await:

import asyncio async def compute(x, y): print(«Compute %s + %s …» % (x, y)) await asyncio.sleep(1.0) return x + y async def print_sum(x, y): result = await compute(x, y) print(«%s + %s = %s» % (x, y, result)) loop = asyncio.get_event_loop() loop.run_until_complete(print_sum(1, 2)) loop.close()

import asyncio

async def compute(x, y):

    print(«Compute %s + %s …» % (x, y))

    await asyncio.sleep(1.0)

    return x + y

async def print_sum(x, y):

    result = await compute(x, y)

    print(«%s + %s = %s» % (x, y, result))

loop = asyncio. get_event_loop()

loop.run_until_complete(print_sum(1, 2))

loop.close()

Класс asyncio Future аналогичен классу concurrent.future.Future. Он не является потокобезопасным и поддерживает следующие функции:

добавление и удаление готовых обратных вызовов

Бесплатный курс «Python. Быстрый старт»

Получите курс и узнайте, как создать программу для перевода текстов на Python

Получить курс

отмена

настройка результатов и исключений

Вот как использовать фьючерсы с циклом событий. Сопрограмма take_your_time() принимает фьючерс и устанавливает свой результат после простоя в течение секунды.

Функций ensure_future() планирует сопрограмму, а wait_until_complete() ожидает выполнения фьючерса. Под капотом она добавляет готовый обратный вызов к фьючерсу.

import asyncio async def take_your_time(future): await asyncio.sleep(1) future.set_result(42) loop = asyncio.get_event_loop() future = asyncio. Future() asyncio.ensure_future(take_your_time(future)) loop.run_until_complete(future) print(future.result()) loop.close()

import asyncio

async def take_your_time(future):

    await asyncio.sleep(1)

    future.set_result(42)

loop = asyncio.get_event_loop()

future = asyncio.Future()

asyncio.ensure_future(take_your_time(future))

loop.run_until_complete(future)

print(future.result())

loop.close()

Это довольно громоздко. Asyncio предлагает задачи, чтобы сделать работу с фьючерсами и сопрограммами более простой. Задача — это подкласс Future, который оборачивает сопрограмму и его можно отменить.

Сопрограмма не должна принимать явный фьючерс и устанавливать его результат или исключение. Вот как выполнить те же операции с помощью задачи:

import asyncio async def take_your_time(): await asyncio.sleep(1) return 42 loop = asyncio. get_event_loop() task = loop.create_task(take_your_time()) loop.run_until_complete(task) print(task.result()) loop.close()

import asyncio

async def take_your_time():

    await asyncio.sleep(1)

    return 42

loop = asyncio.get_event_loop()

task = loop.create_task(take_your_time())

loop.run_until_complete(task)

print(task.result())

loop.close()

Транспорты, протоколы и потоки

Транспорт — это абстракция канала взаимодействия. Транспорт всегда поддерживает определенный протокол. Asyncio предоставляет встроенные реализации для TCP, UDP, SSL и каналов подпроцесса.

Если вы знакомы с сетевым программированием на основе сокетов, вам будет легко освоить транспорты и протоколы. С Asyncio вы получаете стандартное асинхронное сетевое программирование. Давайте рассмотрим печально известный echo-сервер и клиент («hello world» сетевого программирования).

Во-первых, клиент echo реализует класс с именем EchoClient, который является производным от asyncio.Protocol. Он сохраняет свой цикл событий и сообщение, которое он отправляет на сервер при подключении.

При обратном вызове connection_made() он записывает сообщение в транспорт. В методе data_received() он просто выводит ответ сервера, а в методе connection_lost() останавливает цикл обработки событий. При передаче экземпляра класса EchoClient в метод цикла create_connection(), результатом является сопрограмма, которую цикл выполняет, пока не завершится.

import asyncio class EchoClient(asyncio.Protocol): def __init__(self, message, loop): self.message = message self.loop = loop def connection_made(self, transport): transport.write(self.message.encode()) print(‘Data sent: {!r}’.format(self.message)) def data_received(self, data): print(‘Data received: {!r}’.format(data.decode())) def connection_lost(self, exc): print(‘The server closed the connection’) print(‘Stop the event loop’) self. loop.stop() loop = asyncio.get_event_loop() message = ‘Hello World!’ coro = loop.create_connection(lambda: EchoClient(message, loop), ‘127.0.0.1’, 8888) loop.run_until_complete(coro) loop.run_forever() loop.close()

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

import asyncio

class EchoClient(asyncio.Protocol):

    def __init__(self, message, loop):

        self.message = message

        self.loop = loop

    def connection_made(self, transport):

        transport.write(self.message.encode())

        print(‘Data sent: {!r}’.format(self.message))

    def data_received(self, data):

        print(‘Data received: {!r}’.format(data.decode()))

    def connection_lost(self, exc):

        print(‘The server closed the connection’)

        print(‘Stop the event loop’)

        self. loop.stop()

loop = asyncio.get_event_loop()

message = ‘Hello World!’

coro = loop.create_connection(lambda: EchoClient(message, loop),

                              ‘127.0.0.1’, 8888)

loop.run_until_complete(coro)

loop.run_forever()

loop.close()

Сервер работает аналогично за исключением того, что он работает вечно, ожидая подключения клиентов. После отправки Echo-ответа он также закрывает соединение с клиентом и готов к подключению следующего клиента.

Новый экземпляр EchoServer создается для каждого соединения, поэтому даже если несколько клиентов подключаются одновременно, проблемы конфликтов с атрибутом transport не возникнет.

import asyncio class EchoServer(asyncio.Protocol): def connection_made(self, transport): peername = transport.get_extra_info(‘peername’) print(‘Connection from {}’.format(peername)) self.transport = transport def data_received(self, data): message = data. decode() print(‘Data received: {!r}’.format(message)) print(‘Send: {!r}’.format(message)) self.transport.write(data) print(‘Close the client socket’) self.transport.close() loop = asyncio.get_event_loop() # Each client connection will create a new protocol instance coro = loop.create_server(EchoServer, ‘127.0.0.1’, 8888) server = loop.run_until_complete(coro) print(‘Serving on {}’.format(server.sockets[0].getsockname())) loop.run_forever()

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

import asyncio

class EchoServer(asyncio.Protocol):

    def connection_made(self, transport):

        peername = transport.get_extra_info(‘peername’)

        print(‘Connection from {}’.format(peername))

        self. transport = transport

    def data_received(self, data):

        message = data.decode()

        print(‘Data received: {!r}’.format(message))

        print(‘Send: {!r}’.format(message))

        self.transport.write(data)

        print(‘Close the client socket’)

        self.transport.close()

loop = asyncio.get_event_loop()

# Each client connection will create a new protocol instance

coro = loop.create_server(EchoServer, ‘127.0.0.1’, 8888)

server = loop.run_until_complete(coro)

print(‘Serving on {}’.format(server.sockets[0].getsockname()))

loop.run_forever()

Вот результат после подключения двух клиентов:

Serving on (‘127.0.0.1’, 8888) Connection from (‘127.0.0.1’, 53248) Data received: ‘Hello World!’ Send: ‘Hello World!’ Close the client socket Connection from (‘127.0.0.1’, 53351) Data received: ‘Hello World!’ Send: ‘Hello World!’ Close the client socket

Serving on (‘127. 0.0.1′, 8888)

Connection from (‘127.0.0.1’, 53248)

Data received: ‘Hello World!’

Send: ‘Hello World!’

Close the client socket

Connection from (‘127.0.0.1’, 53351)

Data received: ‘Hello World!’

Send: ‘Hello World!’

Close the client socket

Потоки предоставляют высокоуровневый API, который основан на сопрограммах и предоставляет абстракции Reader и Writer. Протоколы и транспорты скрыты, нам нет необходимости определять собственные классы, и нет обратных вызовов. Вы просто ожидаете такие события, как соединение и получение данных.

Клиент вызывает функцию open_connection(), которая возвращает объекты Reader и Writer. Чтобы закрыть соединение, он закрывает Reader.

import asyncio async def tcp_echo_client(message, loop): reader, writer = await asyncio.open_connection( ‘127.0.0.1’, 8888, loop=loop) print(‘Send: %r’ % message) writer.write(message. encode()) data = await reader.read(100) print(‘Received: %r’ % data.decode()) print(‘Close the socket’) writer.close() message = ‘Hello World!’ loop = asyncio.get_event_loop() loop.run_until_complete(tcp_echo_client(message, loop)) loop.close()

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

import asyncio

async def tcp_echo_client(message, loop):

    reader, writer = await asyncio.open_connection(

        ‘127.0.0.1’,

        8888,

        loop=loop)

    print(‘Send: %r’ % message)

    writer.write(message.encode())

    data = await reader.read(100)

    print(‘Received: %r’ % data.decode())

    print(‘Close the socket’)

    writer.close()

message = ‘Hello World!’

loop = asyncio. get_event_loop()

loop.run_until_complete(tcp_echo_client(message, loop))

loop.close()

Сервер также значительно упрощен.

import asyncio async def handle_echo(reader, writer): data = await reader.read(100) message = data.decode() addr = writer.get_extra_info(‘peername’) print(«Received %r from %r» % (message, addr)) print(«Send: %r» % message) writer.write(data) await writer.drain() print(«Close the client socket») writer.close() loop = asyncio.get_event_loop() coro = asyncio.start_server(handle_echo, ‘127.0.0.1’, 8888, loop=loop) server = loop.run_until_complete(coro) print(‘Serving on {}’.format(server.sockets[0].getsockname())) loop.run_forever()

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

import asyncio

async def handle_echo(reader, writer):

    data = await reader. read(100)

    message = data.decode()

    addr = writer.get_extra_info(‘peername’)

    print(«Received %r from %r» % (message, addr))

    print(«Send: %r» % message)

    writer.write(data)

    await writer.drain()

    print(«Close the client socket»)

    writer.close()

loop = asyncio.get_event_loop()

coro = asyncio.start_server(handle_echo,

                            ‘127.0.0.1’,

                            8888,

                            loop=loop)

server = loop.run_until_complete(coro)

print(‘Serving on {}’.format(server.sockets[0].getsockname()))

loop.run_forever()

Работа с подпроцессами

Asyncio также охватывает взаимодействия с подпроцессами. Следующая программа запускает другой процесс Python и выполняет код «import this». Это одна из знаменитых пасхалок Python, она выводит «Zen of Python». Проверьте вывод ниже.

Процесс Python запускается в сопрограмме zen() с использованием функции create_subprocess_exec() и связывает стандартный вывод с пайпом. Затем он перебирает стандартные выходные строки, используя await, чтобы дать возможность выполнить другие процессы или сопрограммы, если выходные данные еще не готовы.

Обратите внимание, что в Windows вы должны установить цикл обработки событий ProactorEventLoop , потому что стандартный SelectorEventLoop не поддерживает пайпы.

import asyncio.subprocess import sys async def zen(): code = ‘import this’ create = asyncio.create_subprocess_exec( sys.executable, ‘-c’, code, stdout=asyncio.subprocess.PIPE) proc = await create data = await proc.stdout.readline() while data: line = data.decode(‘ascii’).rstrip() print(line) data = await proc.stdout.readline() await proc.wait() if sys.platform == «win32»: loop = asyncio.ProactorEventLoop() asyncio.set_event_loop(loop) else: loop = asyncio.get_event_loop() loop.run_until_complete(zen()) Output: The Zen of Python, by Tim Peters Beautiful is better than ugly. Explicit is better than implicit. Simple is better than complex. Complex is better than complicated. Flat is better than nested. Sparse is better than dense. Readability counts. Special cases aren’t special enough to break the rules. Although practicality beats purity. Errors should never pass silently. Unless explicitly silenced. In the face of ambiguity, refuse the temptation to guess. There should be one— and preferably only one —obvious way to do it. Although that way may not be obvious at first unless you’re Dutch. Now is better than never. Although never is often better than *right* now. If the implementation is hard to explain, it’s a bad idea. If the implementation is easy to explain, it may be a good idea. Namespaces are one honking great idea — let’s do more of those!

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

import asyncio. subprocess

import sys

async def zen():

    code = ‘import this’

    create = asyncio.create_subprocess_exec(

        sys.executable,

        ‘-c’,

        code,

        stdout=asyncio.subprocess.PIPE)

    proc = await create

    data = await proc.stdout.readline()

    while data:

        line = data.decode(‘ascii’).rstrip()

        print(line)

        data = await proc.stdout.readline()

    await proc.wait()

if sys.platform == «win32»:

    loop = asyncio.ProactorEventLoop()

    asyncio.set_event_loop(loop)

else:

    loop = asyncio.get_event_loop()

loop.run_until_complete(zen())

Output:

The Zen of Python, by Tim Peters

Beautiful is better than ugly.

Explicit is better than implicit.

Simple is better than complex.

Complex is better than complicated.

Flat is better than nested.

Sparse is better than dense.

Readability counts.

Special cases aren’t special enough to break the rules.

Although practicality beats purity.

Errors should never pass silently.

Unless explicitly silenced.

In the face of ambiguity, refuse the temptation to guess.

There should be one— and preferably only one —obvious way to

do it.

Although that way may not be obvious at first unless you’re

Dutch.

Now is better than never.

Although never is often better than *right* now.

If the implementation is hard to explain, it’s a bad idea.

If the implementation is easy to explain, it may be a good idea.

Namespaces are one honking great idea — let’s do more of those!

Заключение

Python asyncio — это комплексная среда для асинхронного программирования. Она применима в разных областях и поддерживает как низкоуровневые, так и высокоуровневые API. Она все еще относительно молода и не достаточно хорошо изучена сообществом.

Я уверен, что со временем появятся лучшие практики и новые примеры, которые облегчат использование этой мощной библиотеки.

Автор: Gigi Sayfan

Источник: //code.tutsplus.com

Редакция: Команда webformyself.

Бесплатный курс «Python. Быстрый старт»

Получите курс и узнайте, как создать программу для перевода текстов на Python

Получить курс

Каков идиоматический способ программирования асинхронного сокета в Delphi?

@Roddy — Я уже прочитал ссылки, на которые вы указываете, они оба ссылаются на презентацию Paul Tyma «Тысячи потоков и блокировки ввода-вывода — старый способ писать Java-серверы — это снова».

Однако некоторые из вещей, которые не обязательно выходят из презентации Павла, заключаются в том, что он указал -Xss: 48k на JVM при запуске и что он предполагает, что реализация NIO JVM эффективна для того, чтобы она была действительной сравнение.

Indy делает не задание аналогично усохшего и строго ограниченного размера стека. Нет вызовов BeginThread (процедура создания потоков Delphi RTL, которую вы должны использовать для таких ситуаций) или CreateThread (необработанный вызов WinAPI) в кодовой базе Indy.

The default stack size is stored in the PE, and for the Delphi compiler it defaults to 1MB of reserved address space (space is committed page by page by the OS in 4K chunks; in fact, the compiler needs to generate code to touch pages if there are >4K of locals in a function, because the extension is controlled by page faults, but only for the lowest (guard) page in the stack). That means you’re going to run out of address space after max 2,000 concurrent threads handling connections.

Теперь вы можете изменить размер стека по умолчанию в PE с помощью директивы {$ M minStackSize [ maxStackSize]}, но это повлияет на все потоки, включая основной поток. Надеюсь, вы не много рекурсии, потому что 48K или (похоже) не много места.

Теперь, прав ли Павел о неэффективности асинхронного ввода-вывода для Windows, в частности, я не уверен на 100% — я должен был бы это измерить, чтобы быть уверенным. Однако я знаю, что аргументы о том, что потоковое программирование проще, чем асинхронное программирование на основе событий, представляют ложную дихотомию .

Асинхронный код не нужен для управления событиями; он может быть основан на продолжении, как и в .NET, и если вы укажете закрытие в качестве продолжения, вы получите бесплатное состояние для вас. Более того, преобразование из линейного кода стиля нити в асинхронный код продолжения-прохождения-стиля может быть сделано механическим с помощью компилятора (преобразование CPS является механическим), поэтому также нет необходимости в ясности кода.

python — Самый простой способ выполнить асинхронное чтение сокетов из UDP в Python?

Я пытаюсь найти самый простой способ чтения из нескольких (около 100) сокетов датаграмм udp в python. Я смотрел на торнадо, но торнадо рекламирует http / tcp, а не поддержку udp.

Прямо сейчас у меня есть потоки, посвященные каждому сокету udp; однако, это не кажется очень эффективным.

1

jab 2 Июл 2012 в 09:21

5 ответов

Лучший ответ

Я должен признаться, что никогда не использовал его, но, возможно, Twisted подойдет вашим потребностям.

Он поддерживает множество протоколов, даже последовательных соединений.

1

Vincent Hiribarren 2 Июл 2012 в 05:27

asyncoro поддерживает асинхронные сокеты TCP и UDP (среди многих других функций). В отличие от других фреймворков, программирование с использованием asyncoro очень похоже на потоки. Простая программа клиент / сервер UDP для иллюстрации:

import socket, asyncoro

def server_proc(n, sock, coro=None):
    for i in xrange(n):
        msg, addr = yield sock.recvfrom(1024)
        print('Received "%s" from %s:%s' % (msg, addr[0], addr[1]))
    sock.close()

def client_proc(host, port, coro=None):
    sock = asyncoro.AsynCoroSocket(socket.socket(socket.AF_INET, socket.SOCK_DGRAM))
    msg = 'client socket: %s' % (sock.fileno())
    yield sock.sendto(msg, (host, port))
    sock. close()

if __name__ == '__main__':
    sock = asyncoro.AsynCoroSocket(socket.socket(socket.AF_INET, socket.SOCK_DGRAM))
    sock.bind(('127.0.0.1', 0))
    host, port = sock.getsockname()

    n = 100
    server_coro = asyncoro.Coro(server_proc, n, sock)
    for i in range(n):
        asyncoro.Coro(client_proc, host, port)

Asyncoro использует эффективные механизмы опроса, где это возможно. Только с сокетами Windows и UDP он использует неэффективные «select» (но использует эффективные порты завершения ввода-вывода Windows для TCP, если pywin32 установлен).

0

Giridhar Pemmasani 11 Июл 2012 в 18:48

SocketServer имеет встроенный модуль UDP-сервер с опциями потоков и разветвлений.

Еще одним вариантом является использование select . что позволит вам сосредоточиться только на сокетах, где данные уже доступны для чтения.

2

Raymond Hettinger 2 Июл 2012 в 06:22

Я думаю, что если вы настаиваете на использовании ioloop торнадо и хотите выполнять обработку сокетов UDP, вам следует использовать версию IOStream торнадо UDP. Я сделал это с успехом в моих собственных проектах. Немного неправильно называть его UDPStream (поскольку это не совсем поток), но базовое использование должно быть очень простым для интеграции в ваше приложение.

См. Код по адресу: http://kyle.graehl.org/ кодирования / 2012 / 12/ 07 / торнадо — udpstream.html

0

kzahel 7 Дек 2012 в 20:36

Я хотел бы добавить некоторые комментарии к первоначальному вопросу, даже если на него уже есть принятый ответ.

Если у вас несколько соединений, для которых требуется независимая обработка (или, по крайней мере, обработка с небольшой синхронизацией), можно использовать один поток на соединение и блокировать чтение. Современные планировщики не убьют тебя за это. Это довольно эффективный способ обработки соединений. Если вас беспокоит объем памяти, вы можете соответствующим образом уменьшить размер стека потоков (не относится к python).

Потоки / процессы будут оставаться в незанятом состоянии ожидания в течение большей части времени (в ожидании новых данных) и не будут занимать процессорное время.

Если вы не хотите или не можете использовать потоки, вызов select определенно путь. Это также низкоуровневое и эффективное ожидание и в качестве бонуса, дает вам список розеток, которые сработали.

1

Jonas Schäfer 2 Июл 2012 в 06:54

11288120

Программирование Python Socket

Программирование сокетов — одна из самых фундаментальных технологий программирования компьютерной сети . Сокет является конечной точкой двусторонней линии связи между двумя программами, запущенными в сети. Клиент и сервер могут обмениваться данными записи и чтения из своих гнезд. У Python есть довольно простой способ начать с интерфейса сокета. Модуль сокетов Pythons обеспечивает доступ к интерфейсу сокетов BSD . Он доступен для всех современных Unix-систем: Windows, Mac OS X, BeOS, OS / 2 и, возможно, дополнительных платформ.

Программирование Socket Python имеет два раздела:

  1. Программа Socket Socket Python
  2. Программа Python Client Socket

 

Программа Server Socket Program

Программа Socket Socket представляет собой приложение на основе Python Console . Эта программа выступает в роли сервера и слушает запрос клиентов из порта № 8080.

server.bind((LOCALHOST, PORT)) server.listen(1)

server.bind((LOCALHOST, PORT))

server. listen(1)

Когда серверный сокет принимает запрос со стороны клиента, он считывает данные с клиента, а также записывает ответ на подключенную клиентскую программу.

Пример Python Server Socket Example (Server.py)

import socket LOCALHOST = «127.0.0.1» PORT = 8080 server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind((LOCALHOST, PORT)) server.listen(1) print(«Server started») print(«Waiting for client request..») while True: clientConnection,clientAddress = server.accept() print(«Connected clinet :» , clientAddress) data = clientConnection.recv(1024) print(«From Client :» , data.decode()) clientConnection.send(bytes(«Successfully Connected to Server!!»,’UTF-8′)) clientConnection.close()

import socket

LOCALHOST = «127.0.0.1»

PORT = 8080

server = socket. socket(socket.AF_INET, socket.SOCK_STREAM)

server.bind((LOCALHOST, PORT))

server.listen(1)

print(«Server started»)

print(«Waiting for client request..»)

while True:

clientConnection,clientAddress = server.accept()

print(«Connected clinet :» , clientAddress)

data = clientConnection.recv(1024)

print(«From Client :» , data.decode())

clientConnection.send(bytes(«Successfully Connected to Server!!»,’UTF-8′))

clientConnection.close()

Программа Python Client Socket (Client.py)

Python Client Socket подключен к порту 8080 Программы Socket Socket Python и IP-адрес (127.0.0.1) серверного компьютера. Здесь мы приводим 127.0.0.1 , потому что Сервер и Клиент работают на одном компьютере. Если клиентская программа работает на другом компьютере, вы можете указать IP-адрес этой машины.

client.connect(SERVER, PORT)

client. connect(SERVER, PORT)

Когда программа Python Client запустится, она подключится к программе Socket Python Socket Program и ожидает ввода с клиентской стороны. Когда вы вводите сообщение, оно будет отправлено на сервер, а затем вы также увидите ответные сообщения со стороны сервера.

Пример клиента Socket Python (client.py)

import socket SERVER = «127.0.0.1» PORT = 8080 client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect((SERVER, PORT)) client.sendall(bytes(«This is from Client»,’UTF-8′)) data = client.recv(1024) print(data.decode()) client.close()

import socket

SERVER = «127.0.0.1»

PORT = 8080

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

client.connect((SERVER, PORT))

client.sendall(bytes(«This is from Client»,’UTF-8′))

data = client. recv(1024)

print(data.decode())

client.close()

Как запустить эту программу?

Создайте программу Socket Socket Python (Server.py) и Python Client Socket Program (client.py) в двух отдельных файлах. Когда вы закончите кодирование, сначала вы должны запустить Python Server Socket Program из командной строки DOS (консоль), затем вы получите сообщение «Server Started …» и «Ожидание запроса клиента» на экране DOS, где серверная программа.

Следующим шагом является запуск Python Client Socket Program из приглашения (консоли) DOS на том же компьютере или других компьютерах в той же сети. Когда вы запустите клиентскую программу, она установит соединение с сервером и отправит сообщение («Это от клиента») с клиентской стороны. После получения сообщения с клиентской стороны сервер отправляет сообщение клиенту «Успешно подключен к серверу!». Это … теперь вы можете видеть, что клиентская программа и серверная программа обмениваются друг с другом.

Программирование асинхронного сокета (чтение-запись с обеих сторон)
В приведенном выше Пример:е вы можете просто отправить одно сообщение от клиента, а также только одно сообщение возврата с сервера. Если вы хотите неоднократно подключаться и связываться с клиентом и сервером, вы должны реализовать цикл while для отправки и получения данных друг с другом.

Пример Python Server Socket (Server.py)

import socket LOCALHOST = «127.0.0.1» PORT = 8080 server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind((LOCALHOST, PORT)) server.listen(1) print(«Server started») print(«Waiting for client request..») clientConnection,clientAddress = server.accept() print(«Connected clinet :» , clientAddress) msg = » while True: in_data = clientConnection.recv(1024) msg = in_data.decode() if msg==’bye’: break print(«From Client :» , msg) out_data = input() clientConnection. send(bytes(out_data,’UTF-8′)) print(«Client disconnected….») clientConnection.close()

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

import socket

LOCALHOST = «127.0.0.1»

PORT = 8080

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

server.bind((LOCALHOST, PORT))

server.listen(1)

print(«Server started»)

print(«Waiting for client request..»)

clientConnection,clientAddress = server.accept()

print(«Connected clinet :» , clientAddress)

msg = »

while True:

in_data = clientConnection.recv(1024)

msg = in_data.decode()

if msg==’bye’:

break

print(«From Client :» , msg)

out_data = input()

clientConnection.send(bytes(out_data,’UTF-8′))

print(«Client disconnected. …»)

clientConnection.close()

Пример Python Socket Client (client.py)

import socket SERVER = «127.0.0.1» PORT = 8080 client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect((SERVER, PORT)) client.sendall(bytes(«This is from Client»,’UTF-8′)) while True: in_data = client.recv(1024) print(«From Server :» ,in_data.decode()) out_data = input() client.sendall(bytes(out_data,’UTF-8′)) if out_data==’bye’: break client.close()

import socket

SERVER = «127.0.0.1»

PORT = 8080

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

client.connect((SERVER, PORT))

client.sendall(bytes(«This is from Client»,’UTF-8′))

while True:

in_data = client.recv(1024)

print(«From Server :» ,in_data.decode())

out_data = input()

client. sendall(bytes(out_data,’UTF-8′))

if out_data==’bye’:

break

client.close()

 

Следующим шагом является запуск Python Client Socket Program из приглашения (консоли) DOS на том же компьютере или других компьютерах в той же сети. Когда вы запустите клиентскую программу, она установит соединение с сервером и отправит сообщение («Это от клиента») с клиентской стороны. После получения сообщения с клиентской стороны сервер ожидает ввода со стороны сервера. Затем вы можете ввести сообщение со стороны сервера и нажать клавишу ввода. В то же время клиент получает это сообщение и ожидает ввода с клиентской стороны. Снова вы можете ввести сообщение с клиентской стороны и нажать клавишу ввода. Теперь вы можете видеть, как сервер и клиент взаимодействуют неоднократно. Это сообщение вы можете продолжить, чтобы отправить «bye» с клиентской стороны.

 

 

Источник: net-informations.com

 

Глава 2 Использование сокетов Delphi.

О чём не пишут в книгах по Delphi

Читайте также

ГЛАВА 12 Сетевое программирование с помощью сокетов Windows

ГЛАВА 12 Сетевое программирование с помощью сокетов Windows Именованные каналы пригодны для организации межпроцессного взаимодействия как в случае процессов, выполняющихся на одной и той же системе, так и в случае процессов, выполняющихся на компьютерах, связанных друг с

Глава 17 Работа в сети с помощью сокетов

Глава 17 Работа в сети с помощью сокетов По мере того, как компьютерный мир все шире объединяется в единую сеть, важность сетевых приложений все больше и больше возрастает. Система Linux предлагает программный интерфейс сокетов Беркли (Беркли), который уже стал стандартным

17.

7. Ошибки сокетов

17.7. Ошибки сокетов Некоторые значения errno встречаются только при работе с сокетами. Ниже приведен список специфических ошибок сокетов вместе с краткими их описаниями. EADDRINUSE Запрашиваемый адрес уже используется и не может быть переприсвоен. EADDRNOTAVAIL Запрашивается

Пара сокетов

Пара сокетов Пара сокетов (socket pair) для соединения TCP — это кортеж (группа взаимосвязанных элементов данных или записей) из четырех элементов, определяющий две конечных точки соединения: локальный IP-адрес, локальный порт TCP, удаленный IP-адрес и удаленный порт TCP. В SCRIPT

Глава 7 Параметры сокетов

Глава 7 Параметры сокетов 7.1. Введение Существуют различные способы получения и установки параметров сокетов:? функции getsockopt и setsockopt;? функция fcntl;? функция ioctl. Эту главу мы начнем с описания функций getsockopt и setsockopt. Далее мы приведем пример, в котором выводятся заданные по

7.4. Состояния сокетов

7.4. Состояния сокетов Для некоторых параметров сокетов время их установки или получения зависит некоторым образом от состояния сокета. Далее мы обсудим эту зависимость для тех параметров, к которым это относится.Следующие параметры сокетов наследуются присоединенным

7.9. Параметры сокетов TCP

7.9. Параметры сокетов TCP Для сокетов TCP предусмотрены два специальных параметра. Для них необходимо указывать level

Глава 9 Основы сокетов SCTP

Глава 9 Основы сокетов SCTP 9. 1. Введение SCTP — новый транспортный протокол, принятый IETF в качестве стандарта в 2000 году. (Для сравнения, протокол TCP был стандартизован в 1981 году.) Изначально SCTP проектировался с учетом потребностей растущего рынка IP-телефонии, и предназначался,

15.4. Функции сокетов

15.4. Функции сокетов Функции сокетов применяются к доменным сокетам Unix с учетом некоторых особенностей и ограничений. Далее мы перечисляем требования POSIX, указывая, где они применимы. Отметим, что на сегодняшний день не все реализации соответствуют этим

27.3. Программирование сокетов

27.3. Программирование сокетов 27.3.1. Что такое сокет? Сокет — это двунаправленный канал между двумя компьютерами в сети, который обеспечивает конечную точку соединения. «Двунаправленный» означает, что данный могут передаваться в двух направлениях — от клиента к серверу и

Что нового в Delphi 2.0 по сравнения с Delphi 1.0?

Что нового в Delphi 2.0 по сравнения с Delphi 1.0? Выпущенная в феврале 1995 года версия Delphi 1.0 стала первым инструментом для Windows, комбинирующим оптимизирующий компилятор, механизмы визуальной разработки Two-Way-Tools и масштабируемую архитектуру обработки баз данных.  Сегодня сотни

Глава 1 Windows API и Delphi

Глава 1 Windows API и Delphi Библиотека VCL, делающая создание приложений в Delphi таким быстрым и удобным, все же не позволяет разработчику задействовать все возможности операционной системы. Полный доступ к ним дает API (Application Programming Interface) — интерфейс, который система предоставляет

Глава 1.

Что нового в Delphi 4

Глава 1. Что нового в Delphi 4 Delphi 4 представляет следующие новые свойства и усовершенствования:Новые расширения языка.Delphi 4 в язык Object Pascal включены динамические массивы, методы обработки переполнения, установка значения параметров по умолчанию, и многое другое.Менеджер

5.5.1. Концепции сокетов

5.5.1. Концепции сокетов При создании сокета необходимо задать три параметра, тип взаимодействия, пространство имен и протокол.Тип взаимодействия определяет способ интерпретации передаваемых данных и число абонентов. Данные, посылаемые через сокет, формируются в блоки,

5.5.7. Пары сокетов

5.5.7. Пары сокетов Как было показано выше, функция pipe() создает два дескриптора для входного и выходного концов канала. Возможности каналов ограничены, так как с файловыми дескрипторами должны работать связанные процессы и данные через канал передаются только в одном

9.3. Использование OLE в Delphi

9.3. Использование OLE в Delphi Как и многие современные среды программирования, Delphi поддерживает возможность автоматизированной разработки приложений, работающих с различными СОМ-сервисами или серверами. Для более глубокого понимания принципов работы приложений,

Асинхронное программирование и сокеты TCP на C # со Стивеном Клири

Стивен Клири является автором книги «Параллелизм в C # Cookbook» и дипломом Microsoft MVP. Он также написал много сообщений в блогах по асинхронному программированию.

Джереми Это Джереми Юнг, и вы слушаете Software Sessions. В своей карьере мне приходилось заниматься программированием сокетов. Общение со службами или устройствами через необработанный TCP вместо красивой абстракции вроде HTTP.

Я помню, как искал в Интернете ответы и почти ничего не нашел, много сообщений о том, как разговаривать с конечной точкой HTTP или создать веб-сайт с помощью Javascript, но очень мало по необработанному TCP.Но было одно исключение — FAQ от разработчика Стивена Клири.

Я хотел получить его совет о том, как писать приложения для сокетов. Оказывается, он также является автором кулинарной книги Concurrency in C #.

Итак, я начинаю с вопроса, должны ли мы по-прежнему создавать потоки вручную?

Создатель языка программирования Ruby сказал, что сожалеет о добавлении потоков в язык. Остается ли необходимость в ручном создании резьбы в C-Sharp?

Стивен Мальчик, это отличный вопрос.он вообще не хотел нити на языке?

Джереми Итак, я думаю, он не хотел, чтобы общедоступный API-интерфейс вручную создавал поток

Стивен Попался. В этом есть смысл. Одна из часто цитируемых строк из моей книги — как только вы набираете новую ветку. Используйте конструктор потока класса потока. У вас уже есть устаревший код. Эээ, это была одна из моих любимых строк для написания, и ее цитировали несколько раз. Причина в том, что ручные потоки или создание потоков вручную в большинстве случаев больше не нужны.

Если вы работаете в Windows, почти единственный оставшийся вариант использования ручного потока — это просто COM-взаимодействие. Надеюсь, вам не нужно этого делать, но некоторым людям это нужно. Итак (смеется), он все еще существует. В наши дни мы можем сделать гораздо больше, просто поработав с пулом потоков. Гм, есть API более высокого уровня с параллельным программированием и тому подобным.

Я совершенно не знаком с языком rust, который является их стандартной библиотекой, но уверен, что он есть, каждый современный API, подобный этому.вы знаете, если вы не выполняете COM-взаимодействие на этом языке (смеется), было бы логично, что вы не захотите использовать ручной поток.

Я думаю, что это верно практически для всех современных языков.

Джереми Для ясности, это был создатель программирования Ruby.

Стивен Руби, извини, я сказал «Ржавчина»?

Опять же, я тоже не знаком с Rust, боюсь, да. Ruby — это язык программирования гораздо более высокого уровня, и я могу понять, почему они не хотели бы поощрять создание потоков вручную.

Джереми В настоящее время вы можете создавать потоки в Ruby, но есть глобальная блокировка интерпретатора. Таким образом, у вас может быть несколько запущенных потоков, но только один из них может выполняться одновременно. И поэтому они пытаются найти новые способы создания примитивов параллелизма, чтобы как-то обойти это. Но сложность в том, что люди, создавшие приложения Ruby, запускают потоки в своих собственных приложениях. И поэтому, если они изменят поведение, например, позволив некоторым вещам выполняться параллельно, это сломает массу вещей.

И они не могут этого сделать. Так что это своего рода интересная проблема, которую они пытаются решить в будущем.

Стивен Справа. Ruby используется так часто, что это может стать большой проблемой обратной совместимости.

Звучит как большая проблема. Да уж. знаете, в go есть действительно интересный способ выполнения параллелизма. это сильно отличается от других языков. Большинство языков используют более традиционный язык. Вот ваши темы. Вот некоторые вещи более высокого уровня, которые вы можете делать поверх него.

Все потоки выполняются одновременно с общей памятью. Вы знаете, это все типичные шаблоны для большинства языков, но похоже, что путь вперед для Ruby может быть связан с чем-то более похожим на. Я не знаю, хочу ли я сказать что-то вроде модели го, но что-то вроде этого, где у вас есть действительно независимые интерпретаторы, которые не разделяют память напрямую.

Джереми Я думаю, они думают о чем-то подобном, создавая какой-то облегченный процесс во время выполнения, имеющий какой-то ограниченный способ обмена данными между этими процессами выполнения. Так что я думаю, что это тот подход, который они сейчас рассматривают.

Стивен Ага. JavaScript тоже сделал что-то похожее, вы знаете, на самом деле есть целое, я могу ошибиться в терминологии, потому что я не гений Javascript, но я думаю, что веб — это веб-рабочие? они, на самом деле они похожи на отдельный поток, но они не могут напрямую совместно использовать память. Таким образом, вы не столкнетесь со всеми подобными проблемами при одновременном выполнении.

Джереми Ага.На самом деле я разговаривал с одним из членов основной команды Ruby, и они сказали, что, по их мнению, один из. дальновидные вещи, которые делал JavaScript, заключались в том, что он никогда не позволял создавать потоки, верно? У вас был только один-единственный поток в вашем браузере, и именно это позволило им позже придумать такую ​​концепцию веб-воркеров. И поскольку они никогда никому не давали доступа к потокам, они могли заставить всех использовать шаблон цикла событий. Правильно? Потому что, если бы у вас не было асинхронной причины, все было бы просто заблокировано в браузере.

И это также позволило им позже внедрить концепцию веб-воркеров и не нарушать работу всех приложений.

Стивен Справа. Да уж. Я думаю, что принудительное использование одного потока определенно позволило им разработать более продвинутые асинхронные возможности до того, как это появилось во многих других языках. В конечном итоге .NET немного обошел их с помощью async и ждал позже. Но потом JavaScript сразу это понял. Какая отличная идея. Мы тоже это возьмем!

Джереми Мы ведь говорили о параллелизме, верно? по вашим словам, что такое параллелизм?

Стивен Не существует стандарта того, что означают все эти различные термины.Но я всегда старался оставаться последовательным в своих статьях в том, что на самом деле означают термины, параллелизм, асинхронность и параллелизм. Я использую эти термины так, что параллелизм — это любой способ одновременного выполнения нескольких задач.

Итак, параллелизм. Использует несколько потоков для обеспечения параллелизма. Это один из видов параллелизма. Вот как я использую эти термины. и это то, к чему большинство людей приходят к выводу: «О, ну, я делаю больше чем одно дело одновременно.Должно быть более одного потока, потому что это верно для большинства языков на протяжении десятилетий.

Я имею в виду, если вы не занимаетесь асинхронным программированием старой школы, которое было настолько сложно, что большинство людей никогда не беспокоились об этом. Было дешевле покупать большие компьютеры, чем обучать всех разработчиков, изучать и поддерживать ужасно сложное асинхронное программное обеспечение.

В наши дни. Асинхронный код намного проще. Так что это становится все более популярным. Параллелизм — это одновременно несколько вещей.Параллелизм — это одна из форм параллелизма с использованием потоков. А асинхронность — это еще один вид параллелизма, когда вы не зависите от потока, вы знаете, ожидая завершения операции. Хм, вы освобождаете текущий поток, это действительно основная идея асинхронности.

Джереми Я думаю, что одно из возможных заблуждений людей заключается в том, что когда они используют ключевые слова async await, они могут предполагать, что все работает параллельно. Что на самом деле происходит там под капотом?

Стивен Справа.Есть причины, по которым люди так думают, и иногда это может работать параллельно. Итак, вы можете написать код с помощью async и await, который будет работать параллельно. Эм, так что это определенно может сбивать с толку. Но, по сути, я описываю это в первую очередь с помощью ключевого слова async, точно так же, как маркер вашего метода, который действительно сообщает компилятору. Я хочу, чтобы вы взяли мой метод, разрубили его на части и сделали из него конечный автомат. это сделано для упрощения асинхронного программирования.Старые стили асинхронного программирования намного сложнее, а async и await на самом деле просто лучший синтаксис для выполнения асинхронного кода. Итак, это ключевое слово async.

Ключевое слово await принимает аргумент, и это верно для всех языков, использующих async и await. Эээ, значит, это верно для Python и JavaScript, C-Sharp и всех остальных. Ключевое слово await принимает аргумент, который может находиться в ожидании, термин C-sharp is awaitable и ключевое слово await, если это, мм, ожидание на самом деле не завершено, оно вернется из текущего асинхронного метода.И когда он возвращается, он возвращает незавершенную, ожидающуюся задачу в до-диез. И, комбинируя этот шаблон, вы можете создать асинхронный код, который фактически выдает текущий поток. И вся идея на самом низком уровне. Знаете, обычно это какой-то ввод-вывод.

Итак, вы отправили несколько HTTP-запросов, например, это обычный запрос. Вы вызываете какой-то API. Итак, вы отправили байты по сети. Ни один поток не должен сидеть и ждать, пока он вернется. В этом вся идея асинхронного кода.Вместо этого вы ждете этого ответа, и этот поток затем освобождается для других дел. Этот метод возвращается. Поток может делать все, что хочет, а затем эта задача завершается. Имеет ли это смысл? Я не знаю, пытался ли я когда-нибудь раньше описать это, используя только словесные слова.

Джереми Вы выполняете вызов метода, который является асинхронным, и внутри метода у вас будут строки кода, некоторые из которых являются просто обычными синхронными строками кода. Итак, они выполняются сверху вниз, верно? Например, сложить числа или что-то в этом роде.

И затем в методе перед вами могут быть строки кода, которые ожидают.

И всякий раз, когда вы нажимаете на один из них, если он выполняет асинхронный вызов, пример, который мы, возможно, будем использовать для HTTP-клиента, верно?

Вы можете запросить некоторые данные из URL-адреса, и поэтому вы находитесь в этой функции, а HTTP-клиент является асинхронным, и вы помещаете await перед этим асинхронным вызовом, правильно?

Стивен Да.

Джереми Когда я дохожу до этой точки, он идет и делает вызов, а затем операционная система обрабатывает фактический сетевой вызов. Но пока это происходит, я предполагаю, что среда выполнения C или интерпретатор действительно может оставить эту функцию и продолжить работу над чем-то другим. Как, например, рендеринг пользовательского интерфейса. Э-э, пока операционная система все еще занимается этим, этой операцией ввода-вывода, этим сетевым вызовом.

Стивен Ага. Итак, давайте рассмотрим пример. Мне нравится этот пример.

Итак, если у вас есть метод нажатия кнопки, и этот метод является асинхронным, и он вызывает ожидание в HTTPClient Get. Во-первых, каждый асинхронный метод начинает выполняться синхронно.Поэтому, когда вы вызываете как await HTTPClient.Get, это точно то же самое, что и вызов HTTPClient.get без await, возвращающего задачу, а затем выполняя await для этой задачи, чтобы она синхронно отправляла эти данные. А затем, пока он ждет ответа, он возвращает незавершенную задачу, эта задача будет завершена позже, когда операционная система вернет ответ. Сейчас же. Наш код возвращается к нашему коду. Затем наш код выполняет ожидание, которое мы ожидаем от этой задачи. Await смотрит на задачу.Он еще не завершен, поэтому фактически возвращается из обработчика события нажатия кнопки. И в этот момент мы фактически возвращаемся к основному циклу пользовательского интерфейса. Таким образом, пользовательский интерфейс может продолжать реагировать.

Теперь, позже, этот ответ возвращается. Итак, задача, которая была возвращена из клиента get async на HTTP, теперь завершена, а затем это заставляет наш метод, наш обработчик событий, наш обработчик событий нажатия кнопки продолжить выполнение, чтобы возобновить выполнение в точке этого ожидания. Я использую одну фразу: await может приостановить этот метод.Он не приостанавливает поток, он не блокирует поток, но приостанавливает этот метод. А затем, после завершения этой задачи, этот метод продолжает выполнение. А затем возобновляет работу с того места, на котором остановился, и продолжает выполнение. Итак, это пример возобновления асинхронного метода после завершения своей задачи.

Джереми И на протяжении всего этого процесса у вас есть только один поток, верно?

Стивен Да и нет. Гм, вся идея async и await или асинхронного кода в целом заключается в том, что нет потока, который был бы заблокирован, ожидая, что ответ вернется.Теперь есть и другие темы. Итак, операционная система, когда она получает пакет по сети или что-то еще, может потребоваться дождаться нескольких пакетов. Правильно? Так что на самом деле есть поток ThreadPool, который он как бы заимствовал, он проверяет. О, ответ еще не готов? Ладно. Да или нет. так что есть поток пула потоков, который используется очень, очень кратко, вы знаете, в заключение этого.

И это нормальное явление для большинства асинхронных операций, оно также неявно зависит от наличия доступных потоков ThreadPool.Очень кратко используются другие темы. У меня есть статья в моем блоге под названием «Нет темы», которая объясняет это более подробно.

Итак, идея состоит в том, что есть несколько других кратковременно используемых потоков, но идея о том, что есть поток, заблокированный в ожидании возврата этого ответа, этого не происходит с асинхронным кодом, в отличие от синхронного кода. Если вы используете старый веб-клиент, разрешены как синхронные, так и асинхронные методы. Если вы используете метод синхронного веб-клиента, вы блокируете этот поток до тех пор, пока не вернется ответ.

Джереми Но во всей этой последовательности событий пользователю никогда не приходилось серьезно думать об этом потоке. Им никогда не приходилось беспокоиться о том, чтобы запланировать выполнение этого HTTP-запроса. потому что стандартная библиотека или конструкция task и async await, встроенная в .NET, вроде как справилась со всем этим за них.

Стивен Да. И в подавляющем большинстве случаев вам даже не нужно об этом думать. Иногда он возникает, если вы делаете что-то, что полностью истощило ThreadPool.бывают ситуации, когда асинхронные задачи могут не выполняться или их выполнение задерживается из-за отсутствия доступных потоков ThreadPool. Это очень редко, но такое бывает.

Подавляющее большинство, я имею в виду, что более 99% кода не должны об этом беспокоиться. the, настройки по умолчанию в ThreadPool от Microsoft очень хорошо настроены, и они очень внимательно следят за всеми своими метриками и много поработали над значениями по умолчанию.Тем не менее, вы можете их изменить. Есть настройки, которые можно установить, чтобы повлиять на пул потоков. Скажите ему, чтобы всегда было больше минимальных потоков и тому подобное. Но в целом настройки по умолчанию подходят, и ThreadPool со временем настраивается самостоятельно. Так что, если он более загружен, он добавит свои собственные потоки, а когда он менее загружен, он убьет некоторые. И снова Microsoft приложила массу усилий для создания пула потоков, который легко справляется с подавляющим большинством сценариев.

Jeremy И когда вы делаете, скажем, набор HTTP-запросов, скажем, вы сделали четыре подряд, есть ли какая-то гарантия того, когда эти запросы будут выполнены.

Stephen Так что да, вы можете по умолчанию асинхронно использовать только ключевые слова async и await, если вы ждете чего-то одного. Затем дождитесь следующего звонка, затем дождитесь следующего звонка, а затем дождитесь следующего звонка. Да, это всегда будет происходить одно за другим по порядку. асинхронность сохраняет порядок кода.

Теперь есть еще один способ работы с такими вызовами. Иными словами, он может выполнять все четыре вызова одновременно, а вы можете взять все четыре возвращенные задачи и использовать библиотечный метод под названием Task.WhenAll, и это дает вам задачу, которая завершается, когда все четыре задачи выполнены. Таким образом, вы можете сказать просто await Task.WhenAll и вставить в него все четыре задачи. И тогда вы знаете, что все они завершены. А затем, и в этом случае, вы фактически выполняете асинхронную форму параллелизма, когда у вас одновременно отправляются четыре разных запроса. И когда они все вернутся. Все эти ответы возвращаются. Затем ваше резюме кода выполняется после этого ожидания.

Джереми В том случае, когда вы выполняете WhenAll, и они выполняются одновременно. Думаю, мы больше не можем предполагать порядок, потому что, как вы сказали, мы можем считать, что они эффективно происходят одновременно.

Стивен Верно, да. Каждый раз, когда вы используете Task.WhenAll или Task.WhenAny, эти несколько задач могут выполняться одновременно. И это на самом деле как бы возвращение к тому месту, где некоторые люди играли с async и ожидали, например, как консольное приложение.И они могут наблюдать это поведение где:

Эй, вы знаете, у меня все четыре этих метода делают что-то одновременно, и они могут даже быть в разных потоках, потому что консольное приложение по умолчанию будет запускать ваш код в потоке пула потоков после его возобновления из Ждите. Он должен где-то запускать его и в консольном приложении, это логичное место для его запуска.

Итак, многие люди сначала изучают async и await, это не идеально, потому что многие люди, когда они впервые изучают предмет на Си-диез, они запускают новое консольное приложение, чтобы поиграть с ним. .И это совершенно нормально с async и await.

Обычно лучше запустить как проект форм Windows или проект WPF и поиграть с async и await там, потому что этот сценарий отличается. Чем консольное приложение. Многие вещи в консольных приложениях попадают в ThreadPool только потому, что им больше некуда запускаться, тогда как в приложении с пользовательским интерфейсом это более нормальный сценарий для async и await, я бы сказал так.

Джереми Ага.так что C-диез, как язык, существует уже 20 лет. Правильно?

Стивен Я не знаю (смеется)

Джереми Давно (смеется)

Стивен В первые дни. Я отказался прикасаться к нему, поэтому (смеется) Тогда я был строго на три с плюсом.

Джереми За прошедшие годы появилось много способов сделать что-то с точки зрения параллелизма. Если бы вам решать, какие способы выполнения параллелизма вы бы удалили или побудили людей не смотреть на них при создании новых приложений?

Стивен О, мальчик. Отличный вопрос (смеется). Я собираюсь получить здесь немного горячей воды, потому что я не знаю, рассказывал ли я когда-либо кому-нибудь все это, ну, очевидно, класс потоков. на самом деле в наши дни его следует использовать только для COM-взаимодействия. Есть определенные вещи, которые вам никогда не следует использовать.

Гм, конструктор задач, например, вы никогда не должны заново создавать новую задачу. И это то, что, опять же, вы знаете, люди, которые входят в задачу, которые никогда раньше не использовали задачу, что они собираются делать? Вы знаете, самое первое, их первый пример кода будет новичком в задаче, и это, и у них нет возможности узнать, что это полностью неправильно.

Так что конструктор задачи — это определенно то, что никогда не следует использовать. есть много вещей, которые имеют небезопасные значения по умолчанию. Task.Factory.StartNew, вы видите вокруг довольно много. Я действительно не рекомендую его использовать. если вы хотите запустить что-то в ThreadPool, используйте Task. Run. Если вы хотите запустить что-то в другом месте с помощью настраиваемого планировщика, используйте фабрику задач, а не Task.Factory. Это очень разные. Есть некоторое обсуждение самого типа задачи. Я доволен тем, где он находится, но поскольку это могут быть две разные вещи, он может либо представлять код, который может выполняться, либо просто представлять это будущее, либо обещать что-то, что будет завершено в будущем.И второй способ — это то, как он обычно используется в async и await. И первый способ — это то, как он обычно используется в параллельной библиотеке задач. Итак, у вас есть основная задача. Это похоже на основной тип, используемый async и await. Хотя в наши дни у нас есть задачи-значения и другие вещи, которые становятся все более распространенными, но это по-прежнему основной тип, используемый в async и await, а также основной тип, используемый в параллельной библиотеке задач. Но способ его использования в этих двух мирах совершенно разный.

Было некоторое обсуждение, когда Microsoft создавала async и ожидала, должны ли они вместо этого создать новый тип обещания, возможно, а может и нет. Очень сложно оглянуться назад и сказать. Даже сейчас, годы спустя, действительно трудно сказать, да, это было правильное решение, или нет, это было неправильное решение. Я доволен задачей. но еще одна вещь, и именно здесь я могу попасть в горячую воду, которую я бы сказал никогда не использовать, — это все эти методы, которые говорят, запускать этот код в потоке пользовательского интерфейса.

Итак, есть несколько таких способов. Winforms имел Control.Invoke, а затем у диспетчера что-то есть, я сейчас не могу вспомнить, но вы знаете, по сути, я думаю, что это может быть асинхронный вызов диспетчера или основного диспетчера. Итак, в WPF это есть в UWP, все эти разные вещи, можно сказать, запустить этот код обратно в поток пользовательского интерфейса. А это неправильно. Я собираюсь рискнуть и сказать, что не могу придумать никакой веской причины использовать этот код. Это почти всегда делает код менее тестируемым и менее удобным в обслуживании, и есть лучшие решения, если вы просто хотите получить результаты чего-то обратно в поток пользовательского интерфейса, а затем вернуть его и использовать await или что-то в этом роде. Если вы хотите уведомить поток пользовательского интерфейса о прогрессе. Затем используйте IProgress of T. Для этого есть специальный интерфейс. Поэтому я думаю, что эти API поощряли код, который на самом деле не соответствует лучшим практикам и в результате его труднее тестировать. Сложнее использовать повторно.

Джереми Да, я полагаю, вы бы сказали, что это правильный способ сделать это, вместо того, чтобы использовать эти вызовы, которые явно говорят, что мне нужно запустить этот код в потоке пользовательского интерфейса, вы бы сделали что-то большее, неявное, как вы говорили, если у вас есть обработчик событий для чего-то происходящего в пользовательском интерфейсе, например, нажатия кнопки, если вы использовали, async и await, он автоматически вернет вас в поток пользовательского интерфейса всякий раз, когда вы нажмете один из этих вызовов await.И это предпочтительнее, чем говорить, как в примере Control.Invoke, о котором вы говорите.

Стивен Справа. Я думаю, что вам нужен пользовательский интерфейс, контроллеры и тому подобное. В ASP.NET вы хотите, чтобы этот пограничный код управлял вашим бизнес-кодом. Что вам не нужно, так это ваш бизнес-код, управляющий пользовательским интерфейсом. Вы хотите, чтобы пользовательский интерфейс управлял бизнес-кодом и отображал его прогресс, его результаты и тому подобное. Вы же не хотите, чтобы ваш бизнес-код даже знал о существовании пользовательского интерфейса. Джереми Хорошо. Да уж. Допустим, вы нажали кнопку, и это вызвало функцию. Это своего рода ваш бизнес-код. бизнес-код должен использовать этот вызов Control.Invoke, он должен знать, что, эй, мне нужно вернуть свое значение обратно в поток пользовательского интерфейса, но это даже не должно быть его проблемой.

Стивен Справа. А потом здесь обычно что-то вроде передачи бизнес-кода. Вот элемент управления, который вы должны использовать, как вы знаете, для обратной синхронизации с кодом пользовательского интерфейса. И тогда бизнес-код действительно знает о пользовательском интерфейсе, хотя этого не должно. Это больше похоже на чистоту. Я бы не хотел, знаете ли, умереть на холме за это или что-то в этом роде, но я бы предпочел, чтобы этих API не существовало, потому что есть лучшие решения.

Джереми Как насчет других примеров? Нравится WebClient или HTTPClient? Есть ли примеры классов, которые, как вы знаете, были созданы раньше и имели смысл, но теперь вам, вероятно, больше не следует их использовать?

Стивен Что ж, это хороший вопрос.У этого есть более подробный ответ, потому что есть некоторая выгода от возможности запускать синхронный код, иногда даже ввод-вывод. вы знаете, код веб-клиента может быть синхронным, и в некоторых случаях это то, что вам нужно. вам не нужна асинхронность.

И я говорю, скорее, из практических соображений, потому что. Я бы сказал, да, сделайте это асинхронным. Он основан на вводе-выводе. Очевидно, что он должен быть асинхронным, но тогда вы должны как бы управлять этим, ладно, ну, у нас так много времени в день, и весь этот проект уже использует синхронный код.

Стоит ли менять? Ну это совсем другой вопрос. Правильно? У вас есть аспект чистоты, ну да, конечно, он должен быть асинхронным. И затем у вас есть более реальный мир, да, может быть, когда-нибудь, но не сейчас. Итак, я думаю, что именно WebClient?

Я не вижу в этом проблемы. Иногда проще выполнить синхронный вызов, чем выполнить асинхронный вызов, и найти способ заблокировать его. Если вы не готовы перейти на асинхронность в этой части кода.

Джереми Это больше в контексте того, что у вас есть приложение, написанное не с учетом асинхронности, верно. Не обязательно для новых приложений.

В своей книге вы сказали на стороне сервера и привели в качестве примера asp.net. Вы сказали, что параллельная обработка выполняется редко, потому что хост-сервер уже выполняет параллелизм, и по этой причине код на стороне сервера не должен выполнять параллельную обработку.

Что именно вы имели в виду? И как люди должны думать об этом, когда пишут ASP. NET-приложений?

Стивен Это проблема, которую я видел у других людей. Это проблема, которую я сделал. Эээ, даже смущающе. Зная лучше, как после того, как я написал это, у меня действительно была ситуация, когда я хотел действительно сократить время приема-передачи для определенного запроса на asp.net.

Это был один запрос. Вы знаете один путь, и единственный способ сделать это — использовать параллелизм, потому что он связан с процессором. Так что он выполняет большую часть работы центрального процессора. И я действительно пошел дальше.Я сказал, что знаю, что делаю, и сделал это параллельным, и производительность сервера просто упала. Я серьезно. Это было ужасно. И я закончил тем, что вернул его всего пару дней спустя и сказал, хорошо (смеется), урок извлечен, я должен был последовать своему собственному совету.

Причина в том, что ASP.NET изначально является многопоточным сервером, поэтому он уже использует пул потоков. Так что используйте асинхронность в ASP. NET абсолютно. Потому что это освобождает потоки ThreadPool для обработки других запросов. Вы можете обрабатывать гораздо больше запросов, используя асинхронность в ASP.NET, но с использованием параллелизма в ASP.NET, где вы теперь хорошо используете эти несколько потоков вместе, вместо одного потока на запрос или даже ноль в случае некоторых асинхронных вызовов, тогда как в асинхронной части он фактически использует ноль потоков. Итак, вместо использования нуля или одного потока. Теперь вы используете 10, 12 потоков для обработки одного запроса, и это очень быстро отбрасывает ASP.NET.

ASP.NET основан на идее управления потоками. Вы знаете, он передает потоки запросам, и если один запрос внезапно берет 12 потоков из пула потоков, он плохо на это отвечает.

Джереми Использование асинхронных вызовов — это хорошо. Так что использование асинхронного ввода-вывода при выполнении запроса ASP.NET — это хорошо. Но вам, возможно, придется подумать более внимательно, если вы выполняете много задач или используете параллельную библиотеку библиотечные вещи вроде этого.

Стивен Да, именно так. Что-нибудь с Parallel или Task.Run. В большинстве случаев этого следует избегать в ASP.NET. Вы знаете, если вы говорите о внутреннем веб-сайте и знаете, что одновременно не будет более двух пользователей или что-то в этом роде, вы знаете, это особый сценарий, но если вы, если вы получил бы что-нибудь публичное, я бы не стал использовать Parallel или Task.Работать на ASP.NET вообще.

Джереми Если у вас есть что-то, что сильно ограничено процессором, как вы порекомендуете это запустить?

Стивен В ASP.NET? Просто запустите его напрямую, прямо в потоке запросов, вот что я бы сказал. Так что не используйте Task.Run, давайте рассмотрим пример. Просто очень быстро. Async и await позволяют создавать поток. Это позволяет этому потоку вернуться в пул потоков, и это здорово. особенно в ASP.NET, вы знаете, вы можете масштабировать свой сервер, чтобы обрабатывать в 10 раз больше запросов, если все они много раз асинхронны.

Итак, некоторые люди думают, что я хочу использовать async и await, и у меня есть этот привязанный к ЦП или этот код, связанный с ЦП, поэтому я просто вставлю его в Task.Run, а затем я могу дождаться Task.Run. Хорошо. Да. Но посмотрим, что здесь происходит. Ваш запрос запускает выполнение asp.net передает вам поток запроса, ваш запрос начинает выполняться, ваш обработчик начинает выполняться, а затем вы отправляете некоторый код в Task.Run, который берет другой поток из пула потоков для запуска этого кода. И тогда вы ждете задания.run, что позволяет вернуть исходный поток обратно в пул потоков.

Ну, да, вы вернули этот исходный поток, но вы также берете другой поток, и теперь у вас есть, вы знаете, что два потока взаимодействуют, и это на самом деле медленнее. Вы знаете, у вас есть переключение задач и все такое. так что в конце концов он на самом деле медленнее. И вы ничего не решили, используя Task.Run в ASP.NET.

В UI сценарий совсем другой. Правильно? Поскольку у вас есть один особенный поток, поток пользовательского интерфейса, вы хотите использовать асинхронный режим и ждать его, чтобы освободить пользовательский интерфейс, чтобы он оставался отзывчивым. В этом случае вы не хотите напрямую запускать связанный с ЦП код, потому что он заблокирует ваш пользовательский интерфейс на несколько секунд или что-то еще. Вместо этого вы можете бросить его в Task.Run и ждать этого, и теперь вы запускаете это в потоке ThreadPool и ожидаете ответа, позволяя потоку пользовательского интерфейса реагировать.

Таким образом, это отличный шаблон для использования в приложениях пользовательского интерфейса, но не тот, который следует использовать в приложениях ASP.NET.

Джереми Есть ли в этом смысл, я знаю, что в некоторых других языках все еще используется концепция очередей заданий.Если запрашивающие делают вещи, которые довольно сильно ограничены процессором, это займет некоторое время, есть ли какой-либо эквивалент тому, который вы могли бы рассмотреть в asp.net?

Стивен Очереди заданий … Итак, вы говорите, что если приходит запрос, вы хотите что-то сделать, это может занять некоторое время. А пока знаете что, что собираетесь делать? Это запросы, можете ли вы уже отправить ответ, или мы говорим больше о фоновой работе?

Джереми Я думаю, скорее, это могло быть послано, чтобы сказать другому процессу или другому серверу, чтобы как бы сделать работу.и тогда вы могли бы ответить. но тем временем вы задерживаете не только один сервер. Вы знаете, пытаюсь обработать информацию.

Стивен Попался. Правильно. поэтому async и await этого не делают. Некоторые люди. Когда они впервые начинают использовать async и await на asp.net, они думают, что если я жду этого, он должен вернуть обратно, вы знаете, HTTP-ответ клиенту. Но на самом деле это не так. Ух, когда вы ожидаете в async или на asp.net, когда ожидаете, вы фактически просто возвращаете этот поток обратно в ThreadPool.Ответ на самом деле еще не заполнен и не отправлен. Так что в asp.net ничего подобного нет. Но это очень распространенная модель. Итак, у вас очень надежная очередь, когда-то, я бы сказал, MSMQ, хотите верьте, хотите нет. Хм, но (смеется) в наши дни, знаете, это больше похоже на очереди Azure или простые очереди AWS. Вы знаете, что-то вроде надежной очереди, где вы пишете в очередь, а затем вы знаете, что сообщение есть, и затем вы можете вернуться назад, знаете, может быть, какой-то идентификатор или что-то в этом роде.Эй, ты знаешь, это я начал твою работу для тебя. а затем ваш клиент может быть уведомлен или опрошен на предмет ответа или результата этого задания, что может произойти через некоторое время после того, как серверный процесс фактически обработал очередь.

Джереми Когда вы пишете приложение asp.net, вы можете указать, являются ли действия вашего контроллера асинхронными или нет. Я думаю, некоторые люди считают, что они должны сделать все асинхронным, а некоторые просто не знают. Что бы вы там посоветовали?

Стивен Ну, значит, Microsoft по умолчанию.Я думаю, что в наши дни все асинхронно, понимаете, особенно если вы смотрите на ASP.NET Core, вы знаете, вы говорите, дайте мне новый контроллер. Что ж, он дает вам все асинхронные методы. И некоторые люди сомневаются в этом высказывании, почему значения по умолчанию асинхронны? И я думаю, причина в том, что в большинстве случаев они выполняют какие-то операции ввода-вывода, часто с базой данных, верно? Вы либо читаете, либо пишете в базу данных в какой-то момент во время обработки этого запроса. Гм, есть такие, которые этого не делают. Но я думаю, что в наши дни большинство действий контроллеров естественно асинхронны.но дело в том, что если у него нет асинхронной работы, то да, абсолютно синхронно.

Я заметил некоторую путаницу и в другом способе, говоря: ну, должны ли мы просто сделать каждый метод везде асинхронным? Нет, конечно, нет. если вам нужно выполнять только синхронную работу, чем просто синхронизировать ее.

Джереми Что касается конкретно ASP.NET, считаете ли вы, что это стоит когнитивной нагрузки, когда кто-то решает, какой из них следует синхронизировать, а какие — асинхронным, или, по умолчанию, они просто оставят его как асинхронный?

Stephen Что ж, если у вас есть асинхронный метод, например, тот, который на самом деле помечен как async, и вы никогда на самом деле ничего не ждете, компилятор выдаст вам предупреждение, и он скажет: Эй, вы знаете, вы на самом деле здесь не выполняется никакой асинхронной работы. И вы можете использовать это для очистки кода.

Точно так же, если у вас есть синхронный метод, и это способ, которым я предпочитаю преобразовывать код из синхронного в асинхронный, вы найдете какой-то API, который вы вызываете, как запрос к базе данных, и скажете, что он выполняет ввод-вывод, который должен быть асинхронным, а потом ждешь, верно? А затем компилятор скажет: «О, вы используете await без асинхронности», и он поможет вам преобразовать эту сигнатуру метода и все такое. Он скажет прямо в ошибке компилятора: «Эй, ваш метод должен выглядеть вот так, и вы можете взять его прямо оттуда и просто вставить прямо в него».

Джереми Да, на самом деле вам не нужно слишком много думать, потому что визуальная студия или любая другая IDE, которую вы используете, как бы подскажет вам, Эй, вы сделали эту функцию асинхронной, но вы действительно не не нужно, чтобы это было, так что давай, исправь это для меня.

Стивен Справа. Да. Да уж. То, что вы не хотите делать, что я видел, как некоторые люди говорят: ну, мне нужно чего-то подождать, поэтому я просто буду ждать, как какой-то случайный, я видел, как ожидание завершенного task, или вы знаете, await Task.Уступайте, ммм, только потому, что они думают, что им нужно чего-то ждать.

Но это неправильный менталитет. Ваш код действительно синхронный. Так что дайте ему синхронный API.

Джереми Возможно, лет 10 назад вы много писали о написании приложений для сокетов или программировании TCP / IP. Что бы вы посоветовали с текущим API, который предоставляет C #, тем, кто создает и управляет долгосрочными TCP / IP-соединениями? Какие классы им следует использовать и какой должна быть их стратегия?

Стивен Мальчик.Это хороший вопрос. Что касается сокетов TCP / IP, то мой самый первый совет — не делайте этого (смеется). Хм, потому что есть много подводных камней, когда ты начинаешь идти по этой дороге. да, поэтому я бы сказал, вы знаете, сначала постарайтесь изо всех сил не делать этого. использовать HTTP HTTP2, э-э, WebSockets, ну, вы знаете, SignalR или, может быть, использовать что-то в этом роде.

Есть и новые, вы знаете, что Google.

Джереми GRPC?

Стивен Да.Спасибо. GRPC использует что-то подобное, если возможно, у этого есть установленный стандарт, который обрабатывает все действительно низкоуровневые детали за вас. Но если вы действительно не можете этого избежать, а иногда и не можете, я всегда просто использовал класс сокета напрямую по нескольким причинам. Во-первых, он довольно близок к API сокетов Беркли, так что вы действительно можете взять вещи, написанные для подобных систем Unix, и сказать, что все в порядке, так что это примерно эквивалентно этому коду сокета в Си-диез.

И еще вы точно знаете, что он делает.Класс сокета представляет собой очень тонкую оболочку API-интерфейсов Winsock, которые в значительной степени напоминают Беркли с некоторыми более продвинутыми вещами, касающимися асинхронных возможностей. Но да, это то, что я действительно рекомендую.

Я имею в виду, что у них есть потоковые абстракции и тому подобное, с которыми я, честно говоря, не так хорошо знаком. Я всегда в основном использовал класс сокетов или свои собственные подобные оболочки.

Джереми Итак, я считаю, что это их класс TCP-клиента.У них есть асинхронные методы подключения, они дают вам доступ к сетевому потоку, который также имеет асинхронные операции чтения и записи. И я думаю, что вы говорите, что, возможно, не рекомендуете людям это использовать, а вместо этого они используют сокет напрямую

.

Стивен Да, и, опять же, вы знаете, частично это произошло потому, что, знаете, когда я изучал TCP IP, это было примерно в конце девяностых, это было примерно с 98 по 2000, когда я изучал TCP / IP.

И я узнал об этом потому, что никто в компании не хотел его изучать.Нам пришлось добавить его в наш продукт. Больше никто не хотел этому учиться. Все старые программисты были похожи, а не я. Заставьте нового ребенка сделать это. Так что я получил, даже не помню, что это были за книги, но у меня было три таких толстых. Я не шучу и похоронил вас в этих книгах, потому что Интернет не помог. Похоронен в этих книгах, где все уроки, которые я в конце концов поместил в сообщения в блогах, да.

Примерно 10 лет спустя я, наконец, все это написал, но я узнал все это, как и 10 лет назад, из этих огромных толстых книг, потому что этого не было, той информации просто не было.Вы не смогли его найти. Так что это было тяжелое путешествие. И все это было в Беркли, верно? Все использовали розетки Беркли. Это похоже на универсальный, и класс сокета является эквивалентом этого .NET. Так что, может быть, я не знаю, я действительно должен попробовать TCP-клиент. Я говорю о сокетах TCP через два месяца. Я говорю об этом, Это зависит от файла. протокол, который вы используете, но некоторые, многие протоколы требуют, чтобы у вас было непрерывное асинхронное чтение. У вас должно быть постоянное чтение, чтобы выявлять некоторые ошибки. Итак, пока вы делаете это непрерывное чтение, я думаю, что использовать любой из них будет нормально. Лично я не против передачи буферов, а не чтения из потока. Но я думаю, что это главное преимущество TCPClient, верно? Это абстракция потока. Еще одна важная вещь, о которой следует помнить, — это то, что на самом деле существует два потока. Есть входной поток и выходной поток, и вы хотите обрабатывать их полностью отдельно.

Джереми Часто, когда вы работаете с TCP-устройствами, есть поток запросов и ответов, когда вы что-то пишете, и вам нужно что-то вернуть, и вам нужно как бы связать или, я думаю, соединить их вместе .Правильно. какая у тебя, твоя стратегия для этого?

Стивен Ваши более простые протоколы действительно похожи на одну команду, а затем приходит ответ, и у вас никогда не бывает более одной команды в полете. Это просто, потому что ответ всегда по последней команде. Для тех, где вы можете перекрывать друг друга или даже иметь команды, идущие в любом направлении . .. для таких я обычно использую просто словарь. У вас есть этот поиск, вот все запросы, которые у меня есть в полете, а затем, когда один из них вернется, вы можете просто посмотреть его прямо в словаре.

Единственная сложная часть ответа на команду, протоколы, заключается в том, как определить, когда она перестала работать. Это то, о чем мы на самом деле не думаем, но после того, как соединение TCP / IP установлено, нет ничего, что могло бы гарантировать его установление. Одна из вещей или один из API, которые мне действительно не нравятся, — это IsConnected на сокете, потому что на самом деле это не означает, что он подключен. Значит, насколько я знаю, это никак не связано (смеется). Вот что это на самом деле означает. Это не похоже на «Эй, я только что проверил».Это все еще связано. Нет, это не то, что он делает. после того, как TCP-соединение установлено, и эти пакеты пошли туда и обратно, устанавливая соединение, если одна сторона не отправляет данные через это соединение, пакеты больше не проходят через него.

Итак, вы не знаете, был ли этот компьютер перезагружен, если ваше приложение умерло на другом конце, если, вы знаете, возможно, маршрутизаторы вышли из строя между ними, и там больше нет пути. вы не имеете ни малейшего представления об этом, пока не попытаетесь отправить.По этой причине, чтобы обнаружить такие сценарии, многие протоколы ответов на команды похожи на контрольные сообщения.

Так что это похоже на сообщение NOOP. Вы просто отправляете его каждые 10 минут или что-то еще, сколько бы вы ни хотели его разместить. А другая сторона просто говорит: да, я понял. Теперь, когда вы отправляете данные, протокол TCP IP сработает и попытается повторить и тому подобное, если необходимо, а затем, в конечном итоге, вернет вам сообщение об ошибке: « Эй, мы не смогли доставить этот пакет, но вам действительно нужно отправить данные для того, чтобы это произошло.

Джереми Итак, допустим, когда у вас есть куча подключений, которые вы устанавливаете в одно и то же время … Как бы вы структурировали свое приложение для создания всех этих подключений и чтобы убедиться, что между каждым из них изолирован?

Стивен Я работал над системами, которые пытались установить сразу несколько соединений. и главное, вы знаете, что в итоге вы получаете своего рода конечный автомат для каждого из них, и вы хотите, чтобы он был отдельным или имел отдельный конечный автомат для каждого из них.

Джереми И для каждого из них у вас будет отдельный термин, я не знаю, является ли этот термин соединением, но вы сказали, что и для ввода, и для вывода вы будете обрабатывать их отдельно, верно? На что это похоже?

Стивен Да. Да, главное здесь то, что на каждое сокетное соединение на самом деле приходится два потока. И снова, очень часто это упускается из виду, потому что мы думаем, что это один поток. Вы знаете, что мы отправляем данные и получаем данные. На самом деле есть два потока.Есть поток отправки и поток получения.

Я не знаю, подходят ли для них отправка и получение, но с одной стороны, с каждой стороны, есть поток отправки и получения. И это на самом деле отдельные потоки. И в общем случае вы хотите все время читать. Итак, вы постоянно читаете.

Как только одно чтение завершается и дает вам некоторые данные, вы хотите немедленно начать другое чтение, даже если вы не ожидаете какого-либо пакета, потому что хорошо подумайте, например, об этих контрольных пакетах.Когда вы пишете в исходящий поток сокета, эта запись считается завершенной не тогда, когда она достигает другой стороны потока, а когда она фактически попадает на сетевой уровень операционной системы.

На этом ваша запись завершена. Это было успешно. В конце концов, у вас нет дальнейших указаний на то, достигнет ли он своей цели или нет. Если нижележащий сетевой уровень пытается отправить этот пакет, и они не могут найти эту машину, значит, она больше не работает, что бы там ни было, он помечает ошибку в этом сокете.

Но у него нет возможности передать вам эту информацию. Итак, ваш сокет теперь знает, что он находится в состоянии ошибки, но ваша запись уже завершена успешно. Вы не узнаете, что он находится в состоянии ошибки, пока не напишите снова или что-то еще. Если у вас происходит непрерывное чтение, это дает вам возможность операционной системе сказать: эй, вы знаете, этот сокет только что умер. Я пытался отправить данные, но не смог. Ваша розетка теперь мертва. Это непрерывное чтение позволяет вам обнаруживать, когда ошибки возникают немедленно или как можно скорее, а также читать любые фактические входящие пакеты и быть готовыми к этому.

Джереми И это также означает, что как только чтение распознает сбой, вы поймете, что не нужно пытаться писать снова и, возможно, просто воссоздайте сокет. Это то, что вы обычно делаете?

Стивен Да. Да уж. Так что для обработки ошибок, независимо от того, что это за ошибка, вы всегда должны просто закрыть сокет. Это был мой путь к обработке ошибок навсегда (смеется). Просто закрой розетку. Не пытайтесь ничего восстанавливать, просто это конец сокета.Как только возникает ошибка, возникает любая ошибка, а потом обычно хочется немного подождать, на всякий случай, потому что иногда возникают какие-то ошибки, которые могут произойти сразу. Например, если ваш локальный сетевой кабель отключен, а на вашем компьютере нет сети. Операционная система немедленно выдаст вам ошибку, и когда вы попытаетесь подключиться к чему-либо, но вы не хотите, чтобы вы застряли в этом жестком цикле, немедленно повторяя попытку и получая немедленные ошибки. Вообще говоря, вы хотите подождать секунду или две перед повторной попыткой.

Джереми И когда у вас есть своего рода поток вашего приложения или людей, которые могут делать запросы через сокет, нужно ли вам иметь какой-либо семафор или какие-либо ограничения, не позволяющие людям отправлять команды во время предыдущего один отправляется.

Стивен Хорошо. Ну, это зависит от того, что делает ваше приложение. Честно говоря, большая часть моей работы с TCP IP была связана с закрытыми сокетами. Так что это вещи в выделенных сетях, и я думаю, вы могли бы их назвать, например, устройства IOT.В этом сценарии я больше сосредоточился на том, чтобы это работало, а не на безопасности, но особенно для всего, что вам нужно, ограждайте все, что вам нужно.

Если кто-то попытается отправить вам пакет размером два гигабайта, вы знаете, вы должны сказать «нет», или сообщение, это два гигабайта. Итак, есть ограничения. И если вы посмотрите на протокол HTTP, например, в HTTP тоже очень мало защиты.

Они могут отправлять сообщения любой длины, а затем до.Серверы, такие как ASP.NET или IIS, чтобы сказать, ну, мы собираемся ввести разумное ограничение на то, сколько, сколько данных вы можете нам передать, и тому подобное.

Джереми Справа. Я думаю, я больше думаю о том, используем ли мы пример устройства IOT, и скажем, есть команда, например, просто включить свет, и вы можете сделать запрос к устройству, чтобы включить его. на, и он ответит: «Эй, я его включил». Если кто-то делает запрос на включение этого света и пока он находится в процессе отправки, они снова нажимают кнопку и говорят, что я хочу включить его снова.По сути, в том, как работает асинхронное программирование. Будет ли он ждать завершения первого запроса, прежде чем будет сделан второй?

Стивен О, если вы разговариваете с той же розеткой. так что есть абстракция потока для сокетов. Так что один запрос уйдет. И затем, если в вашем коде нет ничего, что могло бы предотвратить это, следующий запрос сразу же последует за ним. Он увидит это как две просьбы включить свет. И, возможно, вам нужно предотвратить это с помощью вашего устройства, а может быть, и нет.Это зависит от устройства. Может быть, вы можете обрабатывать только один запрос за раз или что-то в этом роде и сказать: ну, мы не хотим отправлять следующий запрос, пока не получим ответ.

Если это ограничение этого устройства, то да, ваш код должен управлять этим как своего рода конечный автомат. И, может быть, ты не сможешь NO-OP этого второго, понимаешь, относиться к нему как к такому, пока мы едем в устойчивое состояние вместо отправки команд. Есть разные способы моделирования, которые действительно зависят от устройства, с которым вы работаете.

Джереми Но с точки зрения абстракции потока, это заставит первую запись завершиться до начала второй записи? Это верно?

Стивен Да. Итак, пока вы пишете одну команду, и она полностью написана, а затем приходит вторая команда, тогда да, она будет одна за другой. Это реже, но возможно, что запись может быть частично завершена. И вы не хотите оказаться в ситуации, когда у нас есть одна запись, которая частично завершена, а затем другая команда была записана в этот NetworkStream, что возможно с необработанными сокетами TCP / IP.

Но да, то есть редко, но возможно. Таким образом, вам может понадобиться очередь записи, если вы собираетесь разрешить такое использование.

Джереми Хорошо, об этом не позаботится абстракция потока. На самом деле вам нужно будет отслеживать, я закончил свой первый, второй запрос мне нужно поставить в очередь, пока он не будет выполнен.

Стивен Насколько я знаю. Да. Я не смотрел на внутреннее устройство TCP-клиента. поэтому возможно, что они создают свою собственную очередь, потому что вы можете написать это и раскрыть это как потоковую абстракцию.Это возможно. Я не знаю, делают они это или нет.

Джереми Есть ли еще что-нибудь, что вы можете придумать, что люди обычно ошибаются, когда дело доходит до программирования сокетов?

Стивен Ммм. Да уж. Есть еще одна важная тема, которая на самом деле является очень распространенным вопросом. А это очень много людей. Ну, знаете, как будто в колледже у меня был TCP / IP, и мы узнали все о сетевом стеке, уровнях OSI и всем остальном.По мере того как я узнал, TCP / IP разбивает материал на пакеты и отправляет их по сети, а затем эти пакеты собираются заново. В этом был смысл. Гм, но я думаю об этом понятии пакета, иногда сбивает людей с толку, потому что они понимают идею, и я тоже, когда начал использовать TCP / IP, что я могу отправить пакет, и я могу получить пакет, который совсем не то, что мы делаем, когда пишем с сокетов.

Наша абстракция — это поток. Это не пакет и даже не серия пакетов.На самом деле это ручей. Нет способа отправить пакет, и нет способа определить, где границы пакета, когда вы читаете из потока, или должны были сказать, потому что их больше нет. Я думаю, что идея пакета сбивает людей с толку. Потому что они думают, что я могу отправить сообщение, и это сообщение будет получено целиком за один вызов для получения из сокета или чтения из потока. И это проблема, особенно я думаю, потому что люди пробуют это локально, и операционная система скажет: «Хорошо, да, вы отправили столько данных, я могу отправить их прямо сейчас, это не проблема.Никаких задержек. Все в одном пакете. Я не знаю, попадает ли он вообще на сетевой уровень, честно говоря, он знает, что идет на ту же машину, он действует таким образом локально. А потом они действительно попытались использовать это в реальном мире, и все развалилось, потому что на самом деле это не абстракция пакетов. Это абстракция потока.

Итак, если вы отправляете сообщение, вы должны каким-то образом сказать, насколько большие мои сообщения закодированы в сообщении. Используйте префикс или разделители длины для разделения сообщений.Это два наиболее распространенных способа. HTTP имеет очень странную вещь, когда в одном из текстовых заголовков используется текст ASCII, и это необязательно, поэтому его может и не быть. HTTP. Это было очень рано. В представлениях людей о розетках. И я думаю, что если вы посмотрите сейчас на HTTP2, совершенно другой протокол, гораздо проще программировать. Я думаю.

Джереми Да, это довольно интересно. Так вы говорите в HTTP1. нет никакой гарантии, что вы действительно будете знать, насколько велики сообщения и сколько байтов вам нужно прочитать.Это верно?

Стивен Справа. Я знаю, что это правда по ответу. Я не уверен насчет заголовков запроса, но я знаю, что это правда для ответа, который вы можете отправить, вы знаете, вы можете отправлять в свои заголовки, но не включать заголовок длины содержимого. А затем просто транслируйте кучу данных. И вы знаете, что всякий раз, когда вы закрываете сокет, по сути, это делается.

Джереми Ага. Потому что я предполагаю, потому что HTTP, когда вы делаете запросы, ожидается, что когда вы закончите, вы закроете соединение.

Стивен Изначально да. Первоначально. Да уж. А потом у них были расширения, с помощью которых можно было поддерживать соединение. С HTTP / 2, я думаю, вы могли бы даже сделать мультиплексирование. Итак, вы делаете несколько одновременных запросов по одному и тому же соединению.

Все стало намного сложнее. Но вы можете увидеть разницу между заголовком Content Length, а не при загрузке файла. Потому что, если сервер дал вам длину контента, вы можете увидеть, как заполняется индикатор выполнения.Если он не дал вам длину контента, вы просто видите, как он вращается, верно? Как будто я не знаю, как, как далеко мы продвинулись, но я так далеко зашел, понимаете, потому что это все, вы знаете.

Джереми Это интересно. Да уж. Я никогда, никогда не задумывался о причине этого.

Итак, я думаю, мы начнем заканчивать, но прежде, чем мы это сделаем, есть ли что-нибудь еще об асинхронном программировании, программировании сокетов или о чем-то еще, о чем, по вашему мнению, мы должны были поговорить или упомянуть?

Стивен О, мальчик.Ага. Я бы сказал, используйте async, если можете. Не используйте сырые сокеты, если можете этого избежать (смеется). Ну вот и все. Я думаю.

Jeremy Где люди могут подписаться на вас, узнать, чем вы занимаетесь, и почитать вашу книгу «Параллелизм в C #» ?. Стивен Ага. Так что большинство вещей есть на моем сайте stephencleary.com. У меня есть ссылка на мою книгу оттуда, у меня есть блог, которым я в последнее время пренебрегаю. Я так виноват. Иногда я в Твиттере, о, переполнение стека.Конечно. У меня много переполнения стека. Отвечаю там на множество вопросов.

Джереми Большое спасибо, что пришли на шоу, Стивен.

Стивен Что ж, спасибо, что пригласили меня, Джереми.

Джереми Это был мой разговор со Стивеном. Если вы хотите получить заметки к шоу или стенограмму этого эпизода, вы можете получить их на сайте softwaresessions.com Увидимся через пару недель.

Автостопом по асинхронному программированию — pysheeet

Аннотация

Проблема C10k все еще остается загадкой для программистов, которые ищут способ ее решения. Это.Как правило, разработчики имеют дело с обширными операциями ввода-вывода через поток , epoll или kqueue , чтобы программное обеспечение не ожидало выполнения дорогостоящей задачи. Однако разработка удобочитаемого параллельного кода без ошибок является сложной задачей из-за обмену данными и зависимости от должности. Хотя некоторые мощные инструменты, такие как Valgrind, помогите разработчикам обнаружить тупиковые ситуации или другие асинхронные проблемы, решение этих проблем может занять много времени, когда масштабы программного обеспечения растут большой. Поэтому многие языки программирования, такие как Python, Javascript или C ++ посвящены разработке лучших библиотек, фреймворков или синтаксисов для помощи программисты в правильном управлении параллельными заданиями.Вместо того, чтобы сосредоточиться на том, как использовать современные параллельные API, в этой статье основное внимание уделяется дизайну философия, лежащая в основе шаблонов асинхронного программирования.

Использование потоков — более естественный способ для разработчиков диспетчеризации задач без блокирование основного потока. Однако потоки могут привести к проблемам с производительностью, таким как как блокировка критических секций для выполнения некоторых атомарных операций. Хотя использование цикл событий может повысить производительность в некоторых случаях, написание читаемого кода сложно из-за проблем с обратным вызовом (например,г., обратный вызов ад). К счастью, программирование такие языки, как Python, представили концепцию async / await , чтобы помочь разработчикам писать понятный код с высокой производительностью. На следующем рисунке показаны основные цель, используя async / await для обработки соединений сокетов, таких как использование потоков.

Введение

Обработка операций ввода-вывода, таких как сетевые подключения, является одной из самых дорогих задачи в программе. В качестве примера возьмем простой TCP-блокирующий эхо-сервер. (Следующий фрагмент).Если клиент успешно подключается к серверу без отправляя любой запрос, он блокирует чужие подключения. Даже если клиенты отправляют данные как можно скорее сервер не сможет обрабатывать другие запросы, если нет клиент пытается установить соединение. Кроме того, обработка нескольких запросов неэффективно, потому что тратит много времени на ожидание ответов ввода-вывода от оборудование, такое как сетевые интерфейсы. Таким образом, программирование сокетов с параллелизмом становится неизбежным для управления обширными запросами.

 импортная розетка

s = розетка.сокет (socket.AF_INET, socket.SOCK_STREAM, 0)
s.setsockopt (сокет.SOL_SOCKET, сокет.SO_REUSEADDR, 1)
s.bind (("127.0.0.1", 5566))
s.listen (10)

в то время как True:
    conn, addr = s.accept ()
    msg = conn.recv (1024)
    conn.send (сообщение)
 

Одно из возможных решений для предотвращения ожидания сервером операций ввода-вывода — это отправлять задачи в другие потоки. В следующем примере показано, как создать поток для одновременной обработки соединений. Однако создание множества потоков может потреблять всю вычислительную мощность без высокой пропускной способности.Хуже того, приложение может тратить время на ожидание блокировки для обработки задач в критических разделы. Хотя использование потоков может решить проблемы блокировки для сервера сокетов, другие факторы, такие как загрузка ЦП, важны для программиста, чтобы преодолеть проблему C10k. Следовательно, без создания неограниченного количества потоков цикл событий — еще одно решение для управления подключениями.

 импортная резьба
импортный сокет

s = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt (сокет.SOL_SOCKET, сокет.SO_REUSEADDR, 1)
s.bind (("127.0.0.1", 5566))
s.listen (10240)

обработчик def (conn):
    в то время как True:
        msg = conn.recv (65535)
        conn.send (сообщение)

в то время как True:
    conn, addr = s.accept ()
    t = threading.Thread (цель = обработчик, args = (conn,))
    t.start ()
 

Простой сервер сокетов, управляемый событиями, включает три основных компонента: ввод-вывод. модуль мультиплексирования (например, select), планировщик (цикл) и обратный вызов функции (события). Например, следующий сервер использует высокоуровневый Мультиплексирование ввода / вывода, селекторы внутри цикла для проверки того, выполняется ли операция ввода / вывода. готово или нет.Если данные доступны для чтения / записи, цикл получает ввод / вывод событий и выполнять функции обратного вызова, принимает , читает или записывает , чтобы закончить задачи.

 импортная розетка

из селекторов импортировать DefaultSelector, EVENT_READ, EVENT_WRITE
from functools import partial

s = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt (сокет.SOL_SOCKET, сокет.SO_REUSEADDR, 1)
s.bind (("127.0.0.1", 5566))
s.listen (10240)
s.setblocking (Ложь)

sel = DefaultSelector ()

def accept (s, маска):
    conn, адрес = s.accept ()
    conn.setblocking (Ложь)
    sel.register (conn, EVENT_READ, чтение)

def read (conn, mask):
    msg = conn.recv (65535)
    если не сообщение:
        sel.unregister (conn)
        вернуть conn.close ()
    sel.modify (соединение, СОБЫТИЕ_ЗАПИСЬ, частичное (запись, сообщение = сообщение))

def write (conn, mask, msg = None):
    если сообщение:
        conn.send (сообщение)
    sel.modify (conn, EVENT_READ, читать)

sel.register (s, EVENT_READ, принять)
в то время как True:
    events = sel.select ()
    для e, m в событиях:
        cb = e.data
        cb (e.fileobj, м)
 

Хотя управление подключениями через потоки может быть неэффективным, программа, которая использует цикл событий для планирования задач, не легко читать.Для улучшения кода удобочитаемость, многие языки программирования, включая Python, вводят абстрактные такие концепции, как сопрограмма, будущее или async / await для обработки мультиплексирования ввода-вывода. Чтобы лучше понять программный жаргон и правильно его использовать, следующие разделы обсуждают, что это за концепции и какие проблемы они пытаются решить решить.

Функции обратного вызова

Функция обратного вызова используется для управления потоком данных во время выполнения, когда происходит событие. вызван. Однако сохранить текущий статус функции обратного вызова сложно.Например, если программист хочет реализовать рукопожатие через TCP-сервер, он / она может потребовать где-нибудь сохранить предыдущий статус.

 импортная розетка

из селекторов импортировать DefaultSelector, EVENT_READ, EVENT_WRITE
from functools import partial

s = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt (сокет.SOL_SOCKET, сокет.SO_REUSEADDR, 1)
s.bind (("127.0.0.1", 5566))
s.listen (10240)
s.setblocking (Ложь)

sel = DefaultSelector ()
is_hello = {}

def accept (s, маска):
    conn, адрес = s.accept ()
    conn.setblocking (Ложь)
    is_hello [conn] = Ложь;
    sel.register (conn, EVENT_READ, чтение)

def read (conn, mask):
    msg = conn.recv (65535)
    если не сообщение:
        sel.unregister (conn)
        вернуть conn.close ()

    # проверяем, успешно ли рукопожатие
    если is_hello [conn]:
        sel.modify (соединение, СОБЫТИЕ_ЗАПИСЬ, частичное (запись, сообщение = сообщение))
        вернуть

    # пожать руку
    если msg.decode ("utf-8"). strip ()! = "привет":
        sel.unregister (conn)
        вернуть conn.close ()

    is_hello [conn] = Верно

def write (conn, mask, msg = None):
    если сообщение:
        соед.отправить (сообщение)
    sel.modify (conn, EVENT_READ, читать)

sel.register (s, EVENT_READ, принять)
в то время как True:
    events = sel.select ()
    для e, m в событиях:
        cb = e.data
        cb (e.fileobj, м)
 

Хотя переменная is_hello помогает сохранить статус, чтобы проверить, рукопожатие успешное или нет, код программисту становится сложнее Понимаю. На самом деле концепция предыдущей реализации проста. Это равен следующему фрагменту (блокирующая версия).

 def accept (s):
    conn, addr = s.accept ()
    успех = рукопожатие (conn)
    если не успех:
        conn.close ()

def рукопожатие (conn):
    data = conn.recv (65535)
    если не данные:
        return False
    если data.decode ('utf-8'). strip ()! = "привет":
        return False
    conn.send (b "привет")
    вернуть True
 

Чтобы перенести аналогичную структуру из блокирующей в неблокирующую, функция (или задача) требует моментального снимка текущего статуса, включая аргументы, переменные, и точки останова, когда нужно дождаться операций ввода-вывода.Также планировщик должен иметь возможность повторно войти в функцию и выполнить оставшийся код после Операции ввода-вывода завершены. В отличие от других языков программирования, таких как C ++, Python может легко реализовать концепции, описанные выше, потому что его генератор может сохранить все статусы и повторный вход путем вызова встроенной функции next () . Используя генераторы, обрабатывающие операции ввода-вывода, как в предыдущем фрагменте, но не блокирующие форма, которая называется встроенным обратным вызовом , доступна внутри цикла событий.

Цикл событий

Цикл событий — это планировщик для управления задачами в программе, а не в зависимости от операционных систем. В следующем фрагменте показано, как простое событие цикл для асинхронной обработки соединений сокетов. Концепция реализации для добавления задач в очередь заданий FIFO и регистрации селектора при операциях ввода-вывода не готовы. Кроме того, генератор сохраняет статус задачи, что позволяет чтобы иметь возможность выполнять оставшиеся задания без функций обратного вызова, когда Доступны результаты ввода / вывода.Таким образом, наблюдая за тем, как работает цикл событий, он поможет понять, что генератор Python действительно является формой сопрограмма .

 # loop.py

из селекторов импортировать DefaultSelector, EVENT_READ, EVENT_WRITE

класс Loop (объект):
    def __init __ (сам):
        self.sel = DefaultSelector ()
        self.queue = []

    def create_task (сам, задача):
        self.queue.append (задача)

    def опрос (сам):
        для e, m в self.sel.select (0):
            self.queue.append ((e.данные, Нет))
            self.sel.unregister (e.fileobj)

    def is_registered (self, fileobj):
        пытаться:
            self.sel.get_key (fileobj)
        кроме KeyError:
            return False
        вернуть True

    регистр def (self, t, data):
        если не данные:
            return False

        если данные [0] == EVENT_READ:
            если self.is_registered (данные [1]):
                self.sel.modify (данные [1], EVENT_READ, t)
            еще:
                self.sel.register (данные [1], EVENT_READ, t)
        elif data [0] == EVENT_WRITE:
            если сам.зарегистрирован (данные [1]):
                self.sel.modify (данные [1], EVENT_WRITE, t)
            еще:
                self.sel.register (данные [1], EVENT_WRITE, t)
        еще:
            return False

        вернуть True

    def accept (self, s):
        conn, addr = None, None
        в то время как True:
            пытаться:
                conn, addr = s.accept ()
            кроме BlockingIOError:
                доходность (EVENT_READ, с)
            еще:
                перерыв
        return conn, addr

    def recv (self, conn, size):
        msg = Нет
        в то время как True:
            пытаться:
                msg = conn.recv (1024)
            кроме BlockingIOError:
                доходность (EVENT_READ, conn)
            еще:
                перерыв
        вернуть сообщение

    def send (self, conn, msg):
        size = 0
        в то время как True:
            пытаться:
                size = conn.send (msg)
            кроме BlockingIOError:
                yield (EVENT_WRITE, conn)
            еще:
                перерыв
        размер возврата

    def один раз (сам):
        self.polling ()
        незавершенный = []
        для t, данные в себе.очередь:
            пытаться:
                data = t.send (данные)
            кроме StopIteration:
                Продолжать

            если self.register (t, data):
                незавершенный.append ((t, None))

        self.queue = незавершенный

    def run (self):
        в то время как self.queue или self.sel.get_map ():
            self.once ()
 

Назначая задания в цикл событий для обработки соединений, программирование шаблон похож на использование потоков для управления операциями ввода-вывода, но с использованием планировщик на уровне пользователя.Кроме того, PEP 380 позволяет делегировать генератор, который позволяет генератору ждать, пока другие генераторы закончат свою работу. Очевидно, следующий фрагмент более интуитивно понятен и удобочитаем, чем использование обратного вызова функции для обработки операций ввода-вывода.

 # foo.py
# $ python3 foo.py &
# $ nc localhost 5566

импортный сокет

из селекторов импортировать EVENT_READ, EVENT_WRITE

# import loop.py
from loop import Loop

s = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt (сокет.SOL_SOCKET, сокет.SO_REUSEADDR, 1)
s.bind (("127.0.0.1", 5566))
s.listen (10240)
s.setblocking (Ложь)

loop = Цикл ()

обработчик def (conn):
    в то время как True:
        msg = yield from loop.recv (conn, 1024)
        если не сообщение:
            conn.close ()
            перерыв
        выход из цикла.send (conn, msg)

def main ():
    в то время как True:
        conn, addr = yield from loop.accept (s)
        conn.setblocking (Ложь)
        loop.create_task ((обработчик (conn), None))

loop.create_task ((главная (), Нет))
loop.run ()
 

Используя цикл событий с синтаксисом, yield from , может управлять подключениями без блокирование основного потока, который является использованием модуля, asyncio , до Python 3.5. Однако, используя синтаксис, yield from неоднозначно. потому что это может связать программистов в узлы: почему добавление @ asyncio.coroutine делает генератор стал сопрограммой? Вместо использования дайте обработать асинхронных операций, PEP 492 предлагает, чтобы сопрограмма стала автономная концепция в Python, и именно так новый синтаксис async / await , был введен для повышения удобочитаемости при асинхронном программировании.

Что такое сопрограмма?

Документ

Python определяет, что сопрограммы являются обобщенной формой подпрограмм.Однако это определение неоднозначно и мешает разработчикам понять, что сопрограммы есть. Основываясь на предыдущем обсуждении, цикл событий отвечает за для планирования генераторов для выполнения определенных задач, и это похоже на отправлять задания в потоки. В этом случае генераторы служат как потоки в ответственность за «рутинную работу». Очевидно, что сопрограмма — это термин, обозначающий задачу. который запланирован циклом событий в программе, а не в операционных системах. В следующем фрагменте показано, что такое @coroutine .Этот декоратор в основном преобразует функцию в функцию генератора и с помощью оболочки types.coroutine , чтобы сохранить обратную совместимость.

 импортировать asyncio
импортная инспекция
типы импорта

from functools import wraps
из asyncio.futures import Future

def сопрограмма (func):
    "" "Простой прототип сопрограммы" ""
    если inspect.isgeneratorfunction (func):
        возвращаемые типы. coroutine (func)

    @wraps (функция)
    def coro (* a, ** k):
        res = func (* a, ** k)
        if isinstance (res, Future) или inspect.isgenerator (res):
            res = yield from res
        вернуть res
    возвращаемые типы. coroutine (coro)

@coroutine
def foo ():
    выход из asyncio.sleep (1)
    print ("Привет, Фу")

цикл = asyncio.get_event_loop ()
loop.run_until_complete (loop.create_task (foo ()))
loop.close ()
 

Заключение

Асинхронное программирование через цикл событий становится более простым и читаемость в наши дни благодаря поддержке современного синтаксиса и библиотек. Большинство программирования языки, включая Python, реализуют библиотеки для управления планированием задач через взаимодействие с новым синтаксисом.Хотя новые синтаксисы выглядят загадочно в вначале они предоставляют программистам возможность разработать логическую структуру в их код, например использование потоков. Кроме того, без вызова функции обратного вызова после завершение задачи, программистам не нужно беспокоиться о том, как передать текущий статус задачи, например локальные переменные и аргументы, в другие обратные вызовы. Таким образом, программисты смогут сосредоточиться на разработке своих программ, не тратя зря журнал времени для устранения одновременных проблем.

Асинхронное программирование.Блокирующий ввод-вывод и неблокирующий ввод-вывод — Блог

Это первая статья из серии, посвященной асинхронному программированию. Вся серия пытается ответить на простой вопрос: «Что такое асинхронность?». Вначале, когда я впервые начал разбираться в вопросе, я думал, что знаю, что это такое. Оказалось, что я ничего не знал об асинхронности. Итак, давайте узнаем!

Всего серий:

В этом посте мы будем говорить о сети, но вы можете легко сопоставить ее с другими операциями ввода / вывода (I / O), например, изменить сокеты на файловые дескрипторы.Кроме того, это объяснение не фокусируется на каком-либо конкретном языке программирования, хотя примеры будут приведены на Python (что я могу сказать — я люблю Python!).


Так или иначе, когда у вас есть вопрос о блокирующих или неблокирующих вызовах, чаще всего это означает работу с вводом-выводом. Наиболее частым случаем в наш век информации, микросервисов и лямбда-функций будет обработка запросов. Сразу можно представить, что вы, дорогой читатель, являетесь пользователем веб-сайта, а ваш браузер (или приложение, в котором вы читаете эти строки) является клиентом.Где-то в глубинах Амазонки есть сервер, который обрабатывает ваши входящие запросы и генерирует те же строки, которые вы читаете.

Чтобы начать взаимодействие при таком обмене данными клиент-сервер, клиент и сервер должны сначала установить соединение друг с другом. Мы не будем углубляться в семислойную модель и стек протоколов, участвующих в этом взаимодействии, поскольку я думаю, что все это можно легко найти в Интернете. Что нам нужно понять, так это то, что с обеих сторон (клиента и сервера) есть специальные точки подключения, известные как сокеты.И клиент, и сервер должны быть привязаны к сокетам друг друга и слушать их, чтобы понимать, что другой говорит на противоположной стороне провода.

В нашем общении сервер что-то делает — либо обрабатывает запрос, преобразует разметку в HTML, либо смотрит, где находятся изображения, он выполняет какую-то обработку.

Если посмотреть на соотношение между скоростью процессора и скоростью сети, разница составляет пару порядков. Оказывается, если наше приложение большую часть времени использует ввод-вывод, в большинстве случаев процессор просто ничего не делает.Этот тип приложения называется с привязкой к вводу-выводу. Для приложений, требующих высокой производительности, это узкое место, и об этом мы поговорим дальше.

Есть два способа организовать ввод / вывод (примеры я приведу на основе Linux): блокирующий и неблокирующий .

Кроме того, существует два типа операций ввода-вывода: синхронные и асинхронные.

Все вместе они представляют возможные модели ввода / вывода.

Каждая из этих моделей ввода-вывода имеет шаблоны использования, которые выгодны для определенных приложений.Здесь я продемонстрирую разницу между двумя способами организации ввода-вывода.

Блокировка ввода / вывода

При блокирующем вводе-выводе, когда клиент делает запрос на соединение с сервером, сокет, обрабатывающий это соединение, и соответствующий поток, который читает из него, блокируются до тех пор, пока не появятся некоторые прочитанные данные. Эти данные помещаются в сетевой буфер до тех пор, пока все они не будут прочитаны и не будут готовы к обработке. Пока операция не будет завершена, сервер ничего не может сделать, кроме как ждать .

Самый простой вывод из этого — мы не можем обслуживать более одного соединения в одном потоке. По умолчанию сокеты TCP работают в режиме блокировки.

Простой пример на Python, клиент:

  импортная розетка
import sys
время импорта


def main () -> Нет:
    host = socket.gethostname ()
    порт = 12345

    # создать сокет TCP / IP
    с socket.socket (socket.AF_INET, socket.SOCK_STREAM) как sock:
        в то время как True:
            sock.connect ((хост, порт))
            в то время как True:
                data = str.кодировать (sys.argv [1])
                sock.send (данные)
                time.sleep (0,5)

если __name__ == "__main__":
    assert len ​​(sys.argv)> 1, «Пожалуйста, предоставьте сообщение»
    основной()
  

Здесь мы отправляем на сервер сообщение с интервалом 50 мс в бесконечном цикле. Представьте, что это общение клиент-сервер состоит из загрузки большого файла — для завершения требуется некоторое время.

И сервер:

  импортная розетка


def main () -> Нет:
    host = socket.gethostname ()
    порт = 12345
    
    # создать сокет TCP / IP
    с розеткой.socket (socket.AF_INET, socket.SOCK_STREAM) как sock:
        # привязываем сокет к порту
        sock.bind ((хост, порт))
        # прослушивать входящие соединения
        sock.listen (5)
        print ("Сервер запущен ...")

        в то время как True:
            conn, addr = sock.accept () # прием входящего соединения, блокировка
            print ('Подключено' + str (адрес))
            в то время как True:
                data = conn.recv (1024) # получение данных, блокировка
                если не данные:
                    перерыв
                печать (данные)

если __name__ == "__main__":
    основной()
  

Я запускаю это в отдельных окнах терминала с несколькими клиентами как:

  $ клиент Python.py "клиент N"
  

И сервер как:

  $ python server.py
  

Здесь мы просто слушаем сокет и принимаем входящие соединения. Затем мы пытаемся получить данные от этого соединения.

В приведенном выше коде сервер будет заблокирован одним клиентским подключением! Если мы запустим другой клиент с другим сообщением, вы его не увидите. Я настоятельно рекомендую вам поиграть с этим примером, чтобы понять, что происходит.

Что здесь происходит?

Метод send () попытается отправить все данные на сервер, в то время как буфер записи на сервере будет продолжать получать данные.Когда вызывается системный вызов для чтения, приложение блокируется, и контекст переключается на ядро. Ядро инициирует чтение — данные передаются в буфер пользовательского пространства. Когда буфер становится пустым, ядро ​​снова пробуждает процесс, чтобы получить следующую порцию данных для передачи.

Теперь, чтобы при таком подходе обрабатывать двух клиентов, нам нужно иметь несколько потоков, то есть выделять новый поток для каждого клиентского соединения. Мы скоро вернемся к этому.

Неблокирующий ввод-вывод

Однако есть и второй вариант — неблокирующий ввод / вывод . Отличие очевидно из названия — вместо блокировки любая операция выполняется сразу. Неблокирующий ввод-вывод означает, что запрос немедленно ставится в очередь и функция возвращается. Фактический ввод-вывод затем обрабатывается позже.

Установив сокет в неблокирующий режим, вы можете эффективно опрашивать его. Если вы попытаетесь читать из неблокирующего сокета и нет данных, он вернет код ошибки ( EAGAIN или EWOULDBLOCK ).

На самом деле, этот тип опроса — плохая идея. Если вы запускаете свою программу в постоянном цикле опроса данных из сокета, она потребляет дорогое процессорное время. Это может быть крайне неэффективным, потому что во многих случаях приложение должно быть занято-ждать, пока данные станут доступны, или пытаться выполнить другую работу, пока команда выполняется в ядре. Более элегантный способ проверить, доступны ли данные для чтения, — использовать select () .

Вернемся к нашему примеру с изменениями на сервере:

  импорт выбрать
импортный сокет


def main () -> Нет:
    хост = сокет.gethostname ()
    порт = 12345

    # создать сокет TCP / IP
    с socket.socket (socket.AF_INET, socket.SOCK_STREAM) как sock:
        sock.setblocking (0)
        # привязываем сокет к порту
        sock.bind ((хост, порт))
        # прослушивать входящие соединения
        sock.listen (5)
        print ("Сервер запущен ...")

        # сокетов, из которых мы ожидаем читать
        входы = [носок]
        выходы = []

        а входы:
            # ждем, пока хотя бы один из сокетов будет готов к обработке
            читаемый, доступный для записи, исключительный = select.выбрать (входы, выходы, входы)

            для s в читаемом виде:
                если s - носок:
                    conn, addr = s.accept ()
                    inputs.append (соединение)
                еще:
                    данные = s.recv (1024)
                    если данные:
                        печать (данные)
                    еще:
                        inputs.remove (s)
                        s.close ()

если __name__ == "__main__":
    основной()
  

Теперь, если мы запустим этот код с> 1 клиентами, вы увидите, что сервер не заблокирован одним клиентом, и он обрабатывает все, что может быть обнаружено отображаемыми сообщениями.Опять же, я предлагаю вам попробовать этот пример самостоятельно.

Что здесь происходит?

Здесь сервер не ждет, пока все данные будут записаны в буфер. Когда мы делаем сокет неблокирующим, вызывая setblocking (0) , он никогда не будет ждать завершения операции. Поэтому, когда мы вызываем метод recv , он возвращается в основной поток. Основное механическое различие заключается в том, что send , recv , connect и accept могут возвращаться без каких-либо действий.

При таком подходе мы можем выполнять несколько операций ввода-вывода с разными сокетами из одного и того же потока одновременно. Но поскольку мы не знаем, готов ли сокет к операции ввода-вывода, нам пришлось бы задавать каждому сокету один и тот же вопрос и, по сути, вращаться в бесконечном цикле (этот неблокирующий, но все еще синхронный подход называется I / O мультиплексирование).

Чтобы избавиться от этого неэффективного цикла, нам нужен механизм готовности опроса . В этом механизме мы могли бы опросить готовность всех сокетов, и они могли бы сказать нам, какой из них готов к новой операции ввода-вывода, а какой нет, без явного запроса.Когда любой из сокетов будет готов, мы выполним операции в очереди, а затем сможем вернуться в состояние блокировки, ожидая, пока сокеты будут готовы к следующей операции ввода-вывода.

Существует несколько механизмов готовности к опросу, они разные по производительности и детализации, но обычно детали скрыты «под капотом» и не видны нам.

Ключевые слова для поиска:

Уведомлений:

  • Запуск уровня (состояние)
  • Edge Triggering (состояние изменено)

Механика:

  • выберите () , опрос ()
  • epoll () , kqueue ()
  • EAGAIN , EWOULDBLOCK

Многозадачность

Следовательно, наша цель — управлять несколькими клиентами одновременно.Как обеспечить одновременную обработку нескольких запросов?

Есть несколько вариантов:

Отдельные процессы

Самый простой и исторически первый подход — обрабатывать каждый запрос в отдельном процессе. Этот подход является удовлетворительным, потому что мы можем использовать тот же API блокирующего ввода-вывода. Если процесс внезапно завершится сбоем, это повлияет только на операции, выполняемые в этом конкретном процессе, а не на другие.

Минус — комплексная связь .Формально между процессами почти нет ничего общего, и любая нетривиальная коммуникация между процессами, которые мы хотим организовать, требует дополнительных усилий для синхронизации доступа и т. Д. Также в любой момент может быть несколько процессов, которые просто ждут запросов клиентов. , а это пустая трата ресурсов.

Давайте посмотрим, как это работает на практике. Как только запускается первый процесс (основной процесс / главный процесс), он генерирует некоторый набор процессов в качестве рабочих. Каждый из них может получать запросы на одном сокете и ждать входящих клиентов.Как только появляется входящее соединение, один из обрабатывающих его процессов — принимает это соединение, обрабатывает его от начала до конца, закрывает сокет и затем снова становится готовым для следующего запроса. Возможны варианты — процесс может быть сгенерирован для каждого входящего соединения, или все они могут быть запущены заранее и т.д. Это может сказаться на производительности, но для нас это сейчас не так важно.

Примеров таких систем:

  • Apache mod_prefork ;
  • FastCGI для тех, кто чаще всего использует PHP;
  • Phusion Passenger для тех, кто пишет на Ruby on Rails;
  • PostgreSQL.

Резьбы

Другой подход — использовать потоки операционной системы (ОС). В рамках одного процесса мы можем создать несколько потоков. Блокировку ввода-вывода также можно использовать, потому что будет заблокирован только один поток.

Пример:

  импорт выбрать
импорт потоковой передачи
импортный сокет


обработчик def (клиент):
    в то время как True:
        данные = client.recv (1024)
        если данные:
            печать (данные)
        
    client.close ()

def main () -> Нет:
    host = socket.gethostname ()
    порт = 12345

    # создать сокет TCP / IP
    с розеткой.socket (socket.AF_INET, socket.SOCK_STREAM) как sock:
        # привязываем сокет к порту
        sock.bind ((хост, порт))
        # прослушивать входящие соединения
        sock.listen (5)
        print ("Сервер запущен ...")

        в то время как True:
            клиент, addr = sock.accept ()
            threading.Thread (цель = обработчик, args = (клиент,)). start ()

если __name__ == "__main__":
    основной()
  

Чтобы проверить количество потоков в серверном процессе, вы можете использовать команду linux ps с PID серверного процесса:

  $ ps huH p  | wc -l
  

Операционная система сама управляет потоками и способна распределять их между доступными ядрами ЦП. Потоки легче процессов . По сути, это означает, что мы можем генерировать больше потоков, чем процессов в одной системе. Мы вряд ли сможем запустить 10 000 процессов, но 10 000 потоков — это легко. Не то, чтобы это было эффективно.

С другой стороны, нет изоляции , т.е. если произойдет сбой, это может вызвать сбой не только одного конкретного потока, но и всего процесса. И самая большая трудность заключается в том, что память процесса, в котором работают потоки, разделяется между потоками.У нас есть общий ресурс — память, а это означает, что есть необходимость синхронизировать доступ к ней . Хотя проблема синхронизации доступа к общей памяти — это самый простой случай, но, например, может быть подключение к базе данных или пул подключений к базе данных, что является общим для всех потоков внутри приложения, которое обрабатывает входящие подключения. . Сложно синхронизировать доступ к сторонним ресурсам.

Есть общие проблемы с синхронизацией:

  1. В процессе синхронизации возможно взаимоблокировок, .Тупиковая ситуация возникает, когда процесс или поток переходит в состояние ожидания, потому что запрошенный системный ресурс удерживается другим ожидающим процессом, который, в свою очередь, ожидает другого ресурса, удерживаемого другим ожидающим процессом. Например, следующая ситуация вызовет тупик между двумя процессами: Процесс 1 запрашивает ресурс B у процесса 2. Ресурс B заблокирован во время работы процесса 2. Процесс 2 требует ресурса A от процесса 1 для завершения работы. Ресурс A заблокирован, пока выполняется процесс 1.
  2. Отсутствие синхронизации, когда у нас есть конкурентный доступ к общим данным. Грубо говоря, два потока меняют данные и портят их одновременно. Такие приложения сложнее отлаживать, и не все ошибки появляются сразу. Например, хорошо известный GIL в Python — Global Interpreter Lock — один из простейших способов создания многопоточного приложения. Используя GIL, мы говорим, что все структуры данных, вся наша память защищены одним семафором для всего процесса.В следующей главе мы поговорим о совместной многозадачности и ее реализациях.

В следующем посте мы поговорим о совместной многозадачности и ее реализациях.


Посмотрите мою книгу по асинхронным концепциям:


Следующее сообщение

Есть ли лучший способ использовать асинхронные сокеты TCP в C ++, чем опрос или выбор?

Какого черта, я напишу ответ.

Я проигнорирую терминологию «асинхронный» и «неблокирующий», потому что считаю, что это не имеет отношения к вашему вопросу.

Вы беспокоитесь о производительности при работе с тысячами сетевых клиентов, и вы правы, что беспокоитесь. Вы заново открыли проблему C10K. Когда Интернет был молод, люди видели потребность в небольшом количестве быстрых серверов для обработки большого количества (относительно) медленных клиентов. Существующие интерфейсы типа select / poll требуют линейного сканирования — как в ядре, так и в пользовательском пространстве — по всем сокетам, чтобы определить, какие из них готовы. Если многие сокеты часто простаивают, ваш сервер может тратить больше времени на выяснение того, что делать, чем на выполнение реальной работы.

Перенесемся в сегодняшний день, где у нас есть два основных подхода к решению этой проблемы:

1) Используйте один поток на сокет и просто блокируйте чтение и запись. На мой взгляд, это обычно самый простой способ кодирования, и современные операционные системы довольно хорошо позволяют незанятым потокам спокойно спать, не вызывая каких-либо значительных накладных расходов на производительность. По моему опыту, этот подход очень хорошо работает для сотен клиентов; Я лично не могу сказать, как это будет работать для тысяч.

2) Используйте один из интерфейсов, зависящих от платформы, которые были представлены для решения проблемы C10K. Это означает epoll (Linux), kqueue (BSD / Mac) или порты завершения (Windows). (Если вы думаете, что epoll — это то же самое, что poll , посмотрите еще раз.) Все это будет уведомлять ваше приложение только о фактически готовых сокетах, избегая бесполезного линейного сканирования незанятых соединений. Существует несколько библиотек, которые упрощают использование этих специфичных для платформы интерфейсов, включая libevent, libev и Boost.Asio. Вы обнаружите, что все они в конечном итоге вызывают epoll в Linux, kqueue в BSD и так далее, когда такие интерфейсы доступны.

Превращение асинхронной сети наизнанку

Разработка асинхронных сетевых приложений — интересная проблема с уникальными задачами и достаточным количеством решений, особенно на Python. Но то, как вам традиционно приходилось писать код, чтобы он работал хорошо, может дать вам базу кода, которая, хотя и является надежной, может быть сложной для чтения или выполнения.

Я кратко ознакомлю вас с тем, что такое асинхронная сеть, покажу асинхронную реализацию на основе обратных вызовов с использованием почтенного и превосходного Twisted, а затем, наконец, покажу вам, как новая библиотека asyncio Python использует сопрограммы для решения проблемы в целом. новый способ.

Разблокировка розетки

В большинстве сетевых программ используется модель блокировки, позволяющая программисту строить сетевую логику в ясной, императивной манере от начала до конца.В этой публикации мы создадим клиента для службы приветствия, которому мы отправим имя, а затем получим персональное приветствие. Код нашего клиента выглядит так:

 def get_greeting (имя):
    sock = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
    sock.connect (('', 9999))
    sock.send (имя + b '\ r \ n')

    приветствие = b ''
    не приветствуя. endwith (b '\ r \ n'):
        приветствие + = sock.recv (BUFSIZE)

    ответное приветствие

print (get_greeting (b'World ')) 

def get_greeting (имя): sock = сокет.сокет (socket.AF_INET, socket.SOCK_STREAM) sock.connect ((», 9999)) sock.send (имя + b ‘\ r \ n’) приветствие = b » не приветствуя. endwith (b ‘\ r \ n’): приветствие + = sock.recv (BUFSIZE) ответное приветствие print (get_greeting (b’World ‘))

Для большинства программистов должно быть достаточно ясно, что делает этот код, даже если вы не знаете Python. Мы настраиваем сокет, сразу же отправляем имя, за которым следует пара CR / LF, затем многократно читаем из того же сокета, пока не получим всю строку.Затем возвращается результат. Очень просто, очень легко объяснить.

Проблема при таком сетевом программировании заключается в том, что пока выполняются сетевые вызовы, такие как sock.recv , программа заблокирована ; ничего больше не может произойти, пока этот код либо не вернет, либо не выдаст исключение, даже если сама программа в противном случае полностью простаивает.

Это нормально для простой программы — вы можете, например, запустить столько ее копий, сколько вам нужно для обработки большого количества соединений, именно так строится ряд простых сетевых служб Unix, но современные сетевые программы часто должны иметь дело с координацией множества вещей, а не только одним подключением.Традиционно эта проблема решалась потоками, порождаемыми при создании новых соединений, но введение потоков означает также введение багажа потоков, включая как машинные, так и человеческие затраты на синхронизацию доступа к общим программным ресурсам.

Скручиваем наизнанку

Асинхронная сеть решает эту проблему, выполняя работу в сети в неблокирующем режиме . В этом режиме сетевые вызовы возвращаются немедленно независимо от того, есть ли у них данные или нет.Затем программы собирают ссылки на все свои открытые сокеты и используют системные вызовы, такие как select , чтобы заблокировать их все одновременно, возвращая списки сокетов, которые готовы к чтению или записи, затем обрабатывают каждый сокет по очереди и делают все это снова в цикле событий. Этот поток существенно освобождает программиста от необходимости рассуждать о параллельном доступе к совместно используемым объектам, как в случае с многопоточным решением, поскольку только одна процедура может когда-либо обращаться к этим объектам.Отсутствие необходимости синхронизации доступа также может улучшить работу программы.

Библиотеки

, такие как Twisted, обертывают все это, чтобы сделать его более простым и последовательным в использовании. Это упрощает запуск не только множества серверов одного типа, но и множества сетевых соединений и реализаций протоколов одновременно. Наша реализация получения приветствия теперь выглядит так:

 класс GreetingGetter (LineOnlyReceiver):
    def __init __ (self, factory):
        self.factory = factory
        я.pendingGreeting = Нет

    def connectionMade (самостоятельно):
        self.factory.getterConnected (сам)

    def getGreeting (self, name):
        self.sendLine (имя)
        self.pendingGreeting = Отложено ()
        return self.pendingGreeting

    def lineReceived (self, line):
        если self.pendingGreeting:
            self.pendingGreeting.callback (строка)
            response.stop ()

класс GreetingGetterFactory (ClientFactory):
    def buildProtocol (сам, адрес):
        return GreetingGetter (сам)

    def getterConnected (сам, получатель):
        d = получатель.getGreeting (b'World ')
        d.addCallback (печать) 

класс GreetingGetter (LineOnlyReceiver): def __init __ (self, factory): self.factory = factory self.pendingGreeting = Нет def connectionMade (самостоятельно): self.factory.getterConnected (сам) def getGreeting (self, name): self.sendLine (имя) self.pendingGreeting = Отложено () return self.pendingGreeting def lineReceived (self, line): если сам.ожидает приветствия: self.pendingGreeting.callback (строка) response.stop () класс GreetingGetterFactory (ClientFactory): def buildProtocol (сам, адрес): return GreetingGetter (сам) def getterConnected (сам, получатель): d = getter.getGreeting (b’World ‘) d.addCallback (печать)

Здесь почти наверняка потребуются некоторые пояснения. Twisted поддерживает асинхронное поведение с помощью объектов, называемых Deferred s, которые обещают в конечном итоге вернуть что-то, когда это у нас есть, даже если это что-то может быть недоступно в настоящее время. Отложенные объекты возвращаются вместо этого, и вызывающий затем присоединяет обратные вызовы, которые будут выполняться, когда результат станет доступен.

В этом случае (в GreetingGetter.getGreeting ) у нас еще нет приветствия от сервера, поэтому мы отслеживаем и возвращаем Deferred , который мы перезвоним, когда получим. Фабрика и протокол (наш GreetingGetter ) подключены к реактору Twisted, циклу событий, который вызывает методы при возникновении сетевых событий.Реактор сообщит нашему протоколу, когда данные будут получены; если в нашем протоколе есть ссылка Deferred для возврата приветствия, мы активируем его с помощью метода обратного вызова , который в нашем случае вызывает встроенную функцию Python print , как мы установили ее в заводском getterConnected метод.

Там много чего происходит, но ключевой момент в том, что все наши методы созданы так, чтобы никогда не блокировать выполнение, что позволяет Twisted очень эффективно манипулировать большим количеством сетевой активности.Но, конечно же, также много чего происходит там , потому что, делая наш код полностью управляемым вызовами методов через реактор Twisted, мы фактически вывернули все это наизнанку. Можно ли сделать проще? Безусловно, хотя для этого потребуются некоторые новые функции Python, которых не было, когда Twisted только зарождался.

Войдите в Coroutine

Twisted первым сделал несколько шагов, чтобы упростить этот рабочий процесс с помощью функции, которая позволяет сворачивать обратные вызовы в один метод с использованием генераторов и Python 2.5 (inlineCallbacks, которые я использовал в демонстрации написанной мной библиотеки), хотя по-прежнему требуется много кода, ориентированного на обратный вызов. Действительно впечатляющие успехи были достигнуты только недавно с сопрограммами.

В двух словах, сопрограмма — это функция, выполнение которой может быть приостановлено на время ожидания возвращаемого значения. Сопрограммы используются в новой библиотеке asyncio Python 3 для обработки приостановки выполнения нашего кода, пока мы ждем сетевых событий.

В результате мы можем серьезно упростить наш асинхронный клиент приветствия вплоть до единственной функции сопрограммы:

 @asyncio.сопрограмма
def get_greeting (имя):
    reader, writer = yield from asyncio.open_connection ('', 9999)

    писатель.write (имя + b '\ r \ n')

    приветствие = выход из reader.readline ()
    ответное приветствие

цикл = asyncio.get_event_loop ()
coro = get_greeting (b'World ')
приветствие = loop.run_until_complete (coro)
печать (приветствие) 

@ asyncio.coroutine def get_greeting (имя): reader, writer = yield from asyncio.open_connection (», 9999) писатель.write (имя + b ‘\ r \ n’) приветствие = выход от читателя.readline () ответное приветствие цикл = asyncio.get_event_loop () coro = get_greeting (b’World ‘) приветствие = loop.run_until_complete (coro) печать (приветствие)

Помимо нового API (и того факта, что asyncio предоставляет нам объект reader , который мы можем использовать для чтения строки вместо того, чтобы самостоятельно создавать цикл чтения строки), это концептуально , очень похожий на на наш исходный пример блокировки. . Большое новшество, представленное здесь, — это выход из выражений — они приостанавливают нашу текущую сопрограмму и позволяют другим сопрограммам предоставлять данные, которые, в свою очередь, сами приостанавливаются, пока происходит сетевая магия и выполняются другие отдельные задачи.Они также повсеместно используются в API asyncio, что позволяет нам построить весь наш пример без написания единственного обратного вызова.

Важно отметить, что код в функциях сопрограмм на самом деле не запускается при их вызове; вместо этого возвращается объект сопрограммы, который выполняется циклом событий, когда цикл, наконец, получает его (например, loop.run_until_complete ). yield из операторов внутри сопрограммы передает управление указанной сопрограмме, возвращая управление после завершения делегата.Это означает, что мы фактически не подключаемся и не запускаем протокол, когда назначаем возврат из функции сопрограммы get_greeting на coro ; на самом деле мы только что готовимся к этому. Действие происходит только тогда, когда об этом сообщает цикл событий.

asyncio на самом деле имеет много общего с Twisted и может использоваться очень похожим образом — даже смешивая и сопоставляя код на основе сопрограмм с собственными протоколами asyncio. Я с нетерпением жду возможности увидеть, насколько хорошо он работает для создания более крупных сетевых реализаций с использованием сопрограмм asyncio по умолчанию.По опыту могу сказать, что изучать все тонкости асинхронной системы и то, как все элементы сочетаются друг с другом, — это очень весело, но еще веселее найти способы сделать ту же работу еще лучше.

Программирование сокетов TCP / IP на C # .Net Для программистов и студентов

Изучите программирование сокетов на C # .Net

Получите навыки программирования сокетов компьютерных сетей с помощью потоковых сокетов TCP / IP и станьте более профессиональным программистом.

Каждое видео этого курса охватывает важную концепцию программирования сокетов клиент-сервер и сетевой коммуникации. Готовые к использованию примеры кода C # предоставляются в форме решения Visual Studio для загрузки, а демонстрация после каждой пары лекций покажет вам практическую реализацию концепций, описанных ранее.

К концу этого курса вы сможете создавать программное обеспечение C # (Sharp) .Net, способное отправлять и получать данные через сокеты TCP / IP на одноранговой основе с ключевыми словами async и await.Вы изучите не только программирование сокетов, но и async / await ключевых слов. Курс сделает вас лучшим программистом.

Почему стоит пройти этот курс?

Вам следует пройти этот курс, если вы профессионал (или студент) с некоторым опытом программирования в прошлом, но не понимаете, как компьютерные сети работают на программном уровне (на C #, Java или C ++).

Вы изучите методы, которые могут быть полезны в реальных жизненных ситуациях, с которыми обычно сталкиваются программисты.

Многие студенты университетских курсов по программированию распределенных приложений изучали этот курс в прошлом и оставляли положительные комментарии в обзорах. Посмотрев этот курс, они смогли легко выполнять свои задания самостоятельно.

Раздел 1 доступен бесплатно, он содержит очень полезную информацию, которая может пригодиться любому, независимо от того, знаком ли он с C # .Net или нет.

Минимум авансовой теории

Во многих курсах теория накапливается раньше, чем реальный код, в этом курсе используется подход, основанный на минимуме теории.

Вы изучите основы сетевого программирования и начнете писать код на C # менее чем за 15 минут.

Использование Windows Forms и библиотеки классов

В этом курсе будет использоваться проект библиотеки классов на C # вместе с Win Forms, что отличается от подхода многих других курсов, в которых используются проекты командной строки. Моя методика преподавания делает курс намного менее скучным и внеклассным, практически полезным и действительно подходящим для профессионалов.

Коротко, мило, по делу

Весь курс разработан для занятых профессионалов, а видеоролики созданы для того, чтобы сделать ваше онлайн-обучение плодотворным и легким.Это обучение на основе проектов.

Асинхронное программирование с ключевыми словами async / await, современное реальное решение

async / await ключевых слов были введены в C # 5.0. В традиционных сценариях программирования сокетов (многопоточность) используется для создания отзывчивого сервера или клиента. Я обошел этот путь и показал вам, как напрямую использовать асинхронные сокеты. Сначала я объясню вам, что такое ключевые слова async / await, а затем покажу, как их использовать для неблокирующего сетевого ввода-вывода.Эта часть головоломки является ключом к созданию корпоративных приложений с высокой посещаемостью.

Усиленное обучение

В конце каждого раздела есть викторина, которая очень полезна, чтобы убедиться, что вы просматриваете и сохраняете важную информацию, представленную в соответствующем разделе курса.

Присоединяйтесь к активному сообществу

Станьте частью сообщества программистов, которые уже прошли этот курс. На ваши вопросы ответят сокурсники и преподаватель курса.Действительно прекрасное место для начала обучения!

Узнай что-нибудь новое

Сокеты считаются сложной темой, опасной зоной в языке программирования. Знание этого означает, что вы серьезно относитесь к своей торговле.

Объектно-ориентированное программирование

Этот курс научит вас создавать распределенные приложения, используя принципы ООП . Вы собираетесь преодолеть разрыв между внутренней библиотекой классов C # .Net и внешним приложением WinForms, используя реализацию модели Publisher / Subscriber на основе классов EventHandler.Этот курс также покажет вам, что такое обработчики событий и как создать собственное событие.

Бонус

Вы также узнаете, как преобразовать имя хоста в IP-адрес с помощью класса System.Net.DNS. Вы также реализуете различные проверки работоспособности, используя tryparse и try / catch .

Вам не нужно изучать C, чтобы работать над этим курсом.

Сокет Java, сокет Python, UDP

Какую стратегию ввода-вывода мне следует использовать?

Какую стратегию ввода-вывода мне следует использовать?

Уоррен Янг

Существует несколько различных соглашений для связи с Winsock, и у каждого метода есть свои преимущества.Вопрос о час, каковы эти преимущества и как кто-то выбирает соглашение, которое имеет наибольший смысл для их применения? В варианты:

  • Блокировка розеток — По умолчанию блокирует вызов Winsock, что означает, что он не вернется, пока выполнил свою задачу или потерпел неудачу при попытке.
  • Pure Неблокирующие розетки — Обращения к неблокирующим сокетам немедленно возвращаются, даже если они не могут выполнить свою задачу немедленно.Хотя это позволяет программа для выполнения других задач, пока сетевые операции завершаются, это требует, чтобы программа неоднократно опрашивала, чтобы найти выходят, когда каждый запрос завершен.
  • Розетки асинхронные — Это неблокирующие сокеты, за исключением того, что у вас нет для опроса: стек отправляет программе специальное оконное сообщение всякий раз, когда происходит что-то «интересное».
  • select () — Вызов функции select () способ заблокировать поток, пока не произойдет что-то интересное любой из группы розеток.Обычно используется с неблокирующим сокеты, чтобы избежать опроса.
  • Объекты событий — используется с WSAEventSelect () , этот механизм похож на метод select () , но немного эффективнее. Он также работает только на платформах с Winsock, тогда как select () работает на любой платформе с сокетами BSD.
  • Overlapped I / O — Один из Майор Winsock 2 особенность заключается в том, что он связывает сокеты в унифицированный Механизм ввода-вывода.В частности, теперь вы можете использовать перекрывающийся ввод-вывод на сокеты, которые по своей сути более эффективны, чем указанные выше опции.

Еще больше сбивает с толку проблема потоков, потому что каждая из вышеперечисленных механизмы меняются по своей природе при использовании с потоками.

Пытаясь найти ответ на вопрос «какая стратегия ввода-вывода», становится очевидным, что существует всего несколько основных видов программ, а успешные следуют тем же образцам. Из этих шаблонов и практический опыт — личный и заимствованный — Я вывел следующий набор эвристик.Ни один из этих эвристика — это абсолютные законы, одной изолированной эвристики недостаточно, и эвристика иногда противоречит. Когда две эвристики противоречат друг другу, вам нужно решить, что важнее для вашего приложения и игнорировать другого. Однако остерегайтесь игнорировать эвристику просто ведь его нарушение не влечет заметных последствий для вашего программа. Если вы привыкнете игнорировать определенную эвристику, он становится бесполезным.

Эвристика упорядочена с точки зрения совместимости, затем скорости, и, наконец, функциональность.Совместимость прежде всего, потому что если данная стратегия ввода-вывода не будет работать на платформах, которые вам нужны поддержка, не имеет значения, насколько она быстрая или функциональная. Скорость следующий, потому что требования к производительности легко определить, и часто важно. Функциональность на последнем месте, потому что, как только вы решите проблемы совместимости и скорости, ваш выбор становится намного больше субъективно.

Эвристика 1: сузьте свой выбор, определив, какие операционные системы вам нужно поддержать.

Существует множество версий Windows, но когда дело доходит до сетевой стек, вы можете поместить большинство из них в одну из двух групп: производные Windows 95 и Windows NT 4.0 производных. В этой статье рассматривается все остальное — Windows NT 3.x, Win16, Windows CE и платформы, отличные от Windows — отдельно.

Возможно, ваш код также должен быть совместим с системами на основе POSIX. Сюда входят Unix, Linux, MacOS X, QNX и BeOS. Хотя там — это несколько различных сетевых и потоковых API, используемых различными Системы на основе POSIX, я буду говорить только о сокетах BSD и POSIX. темы в этой статье.

Ни одна из этих операционных систем не имеет точно такой же набор сетевые функции.Вы можете использовать этот факт, чтобы исключить ввод-вывод. стратегии, которые поддерживаются не всеми вашими целевыми операционными системами.

Win9x WinCE WinNT 4+ WinNT 3.x Win16 Unix
Розетки с блокировкой да да да да да да
Розетки без блокировки да да да да да да
Асинхронные розетки да да да да
Объекты событий да да
перекрывающийся ввод / вывод да 1 да 2
Резьба да да да да да 3
  1. Win9x не поддерживает перекрывающийся ввод-вывод в ядре.куда перекрывающиеся вызовы ввода-вывода работают в Win9x, потому что механизм эмулируется на уровне API. (Это относится к Winsock, file и по крайней мере последовательный / параллельный порт ввода-вывода.) Это означает, что программы, используйте только перекрывающиеся функции ввода-вывода, гарантированные Winsock spec будет нормально работать на Win9x. Если, с другой стороны, вы заблудитесь функциональность, которую предоставляет только WinNT 4+, ваше приложение сбой на Win9x. Один из примеров — вызов ReadFile () с сокетом: это нормально работает на NT4 +, но не работает на Win9x.
  2. Если вам нужна только поддержка ввода-вывода с разбросом / сборкой, сокеты BSD предоставляет эту функциональность в readv () и writev () звонки. Не существует стандартного механизма Unix, обеспечивающего аналогичный эффективность перекрывающегося ввода-вывода Win32. Некоторые Unix-системы предоставляют семейство функций aio _ * () (называемое асинхронным вводом-выводом, но не связаны с асинхронным вводом-выводом Winsock), но это в настоящее время широко не применяется.
  3. Хотя все современные Unix-системы поддерживают потоки POSIX, все еще много старых Unix-машин со сломанными, нестандартная или несуществующая резьба.Вам нужно будет выбрать подмножество всех Unix, если вы хотите использовать ту же потоковую передачу код на всех Unix. Вы определенно будете писать по-другому потоковый код для Windows, поскольку его потоковый API полностью другой.
Эвристика 2: Избегайте select ().

select () — наименее эффективный способ управления неблокирующими I / O, потому что есть много накладных расходов, связанных с функция. Большая часть этих накладных расходов является линейной функцией числа подключений: удвойте количество подключений, и вы удвоите время обработки.

Примерно единственный раз, когда вы должны использовать select () — для причины совместимости: это единственная неблокирующая стратегия ввода-вывода который работает на всех версиях Windows (включая CE) и практически на все системы на базе POSIX. Если ваша программа должна работать только на не-CE версии Windows, есть альтернативы получше.

Эвристика 3: Асинхронные сокеты лучше всего работают с небольшими объемами данных.

Асинхронный ввод-вывод Winsock ( WSAAsyncSelect () ) не является самая эффективная стратегия ввода-вывода, но не менее эффективная, либо.Это отличный вариант для программы, которая занимается объемы данных. По мере увеличения объема данных накладные расходы становятся более значительный.

Эвристика 4: для высокопроизводительных серверов предпочтительнее перекрывающийся ввод / вывод.

Из всех различных стратегий ввода-вывода перекрывающийся ввод-вывод имеет спектакль. (Порты завершения ввода-вывода еще более эффективны, но нестандартные по сравнению с Winsock, поэтому я не рассматриваю их в FAQ.) При осторожном использовании перекрывающегося ввода / вывода (и большого количества памяти в сервере!) вы можете поддерживать десятки тысяч подключений к одному серверу.Никакого другого ввода / вывода Стратегия приближается к масштабируемости перекрывающегося ввода-вывода.

Эвристика 5: для поддержки умеренного количества подключений, рассмотрите асинхронные сокеты и объекты событий.

Если ваш сервер должен поддерживать только умеренное количество подключений — до 100 или около того — вам может не понадобиться перекрытие Ввод / вывод. Перекрывающийся ввод-вывод нелегко запрограммировать, поэтому, если вы не нужна его эффективность, вы можете избавить себя от множества проблем, используя более простую стратегию ввода-вывода.

Правильно запрограммированные, асинхронные сокеты — разумный выбор для выделенного сервера, поддерживающего умеренное количество подключений.В основная проблема с этим состоит в том, что на многих серверах нет пользовательский интерфейс и, следовательно, отсутствие цикла сообщений. Сервер без UI использование асинхронных сокетов должно было бы создать невидимое окно исключительно для поддержки своих асинхронных сокетов. Если ваша программа уже имеет пользовательский интерфейс, однако асинхронные сокеты могут быть наименее болезненный способ добавить к нему функцию сетевого сервера.

Еще один разумный выбор для работы с умеренным количеством соединения — это объекты событий. Они очень эффективны в самих себя.Основная проблема, с которой вы сталкиваетесь с ними, заключается в том, что вы не может блокировать более 64 объектов событий одновременно. Чтобы заблокировать более того, вам нужно создать несколько потоков, каждый из которых блокируется на подмножество объектов событий. Прежде чем выбрать этот метод, рассмотрите для обработки 1024 сокетов требуется 16 потоков. Каждый раз, когда у тебя есть гораздо больше активных потоков, чем у вас есть процессоров в системе, вы начинаете вызывать серьезные проблемы с производительностью.

Одно предупреждение: очень легко недооценить количество одновременные подключения вы получите на общедоступном Интернет-сервере.Это может иметь смысл спроектировать с учетом масштабируемости, даже если ваши оценки в настоящее время не предсказывают тысячи одновременных клиентов.

Эвристика 6: серверы с низким трафиком могут использовать почти все Стратегия ввода-вывода.

Для серверов с низким трафиком особой необходимости суперэффективный. Возможно, ваш сервер просто не видит высоких трафик, или, возможно, он работает под управлением производной Windows 95 и поэтому он ограничен 100 сокетами одновременно ОС. Подходящие стратегии для 1-100 подключений: объекты событий, неблокирующие сокеты с select () , асинхронные розетки, и резьбы с блокировкой розеток.

Мы уже рассмотрели первые три метода, поэтому давайте рассмотреть потоки с блокирующими сокетами. Часто это самый простой кстати однозначно писать сервак. У вас просто есть основной цикл, который принимает соединений и отделяет каждое новое соединение от собственного потока, где он обработан блокирующими розетками. Блокирующие розетки имеют несколько преимущества. Они эффективны, потому что, когда поток блокируется, операционная система немедленно позволяет запускать другие потоки. Также синхронный код более простой, чем эквивалентный несинхронный код.

Есть две основные проблемы с потоком на соединение серверы. Во-первых, потоки часто требуют большой работы по синхронизации, что трудно понять; это может превзойти преимущества простоты использования блокирующих розеток. Во-вторых, потоки плохо масштабируются вообще: по мере увеличения количества потоков операционная система накладные расходы, связанные с переключением контекста между потоками, становятся существенный. Этот метод подходит только для небольшого числа людей. подключений или большее количество подключений, которые в основном холостой ход.

Эвристика 7: Не блокировать внутри пользовательского интерфейса нить.

Эта эвристика больше похожа на простое правило Программирование для Windows, но я поднимаю его, потому что большинство программ однопоточный. В однопоточной программе с графическим интерфейсом каждый раз, когда вы вызываете функция Winsock, которая блокирует поток пользовательского интерфейса, кнопки не могут быть нажата, меню не раскрывается, полосы прокрутки не перемещаются, нажатия клавиш игнорируются … ваш интерфейс зависает.

Эвристика 8: для клиентских программ с графическим интерфейсом асинхронные сокеты.

Есть две причины для этой эвристики:

  1. Асинхронные розетки были разработаны с самого начала для работы хорошо с программами GUI. У вас уже есть оконная петля, и у вас уже есть код управления окнами в остальной части программа. Добавить асинхронный сетевой ввод-вывод так же просто, как добавление диалога в вашу программу.
  2. Все альтернативы требуют как минимум одного дополнительного поток для обработки сети, чтобы удовлетворить предыдущий эвристический.С помощью асинхронных сокетов вы можете обрабатывать как сеть и пользовательский интерфейс с одним потоком. Поскольку сообщения окна обрабатываются по одному в порядке их поступления, все автоматически синхронизируется.
Эвристика 9: потоки редко помогают клиенту программы.

Когда программист впервые узнает о потоках, он стремится опробовать их в его собственных программах. Он видит, что у них несколько достоинств, но недостатков пока не видит. к несчастью для новичка, который скоро получит образование, эти недостатки могут значительные последствия.

Одно из реальных преимуществ потоков состоит в том, что поток, выполняющий ввод-вывод на блокирующий сокет имеет линейный поток управления, и поэтому его легче понимать. Асинхронный код более разложен, поэтому сложнее писать и отлаживать.

Еще одно очевидное преимущество потоков — это своего рода инкапсуляция: программист может разбить программу на несколько потоков, каждый из которых который имеет одну четко определенную задачу. Но это действительно только в том случае, если каждый поток в основном не зависит от остальной части программы.Если не, потоки должны будут обмениваться данными через общую структуру данных, уничтожение любой потенциальной инкапсуляции.

В конце концов, самая большая проблема с потоками также связана с общие структуры данных: синхронизация. Этот вопрос освещен лучше в другом месте, так что я не буду тратить много слов на это здесь. Короче, синхронизацию сложно понять: плохо синхронизируемые потоки подвержены задержкам сериализации, накладным расходам на переключение контекста, взаимоблокировки, состояния гонки и поврежденные данные.Это тяжелые проблемы, и для большинства программ выгода недостаточна, чтобы сделать их стоит преодолеть.

Более разумной альтернативой является использование асинхронного ввода-вывода. Это покупает вам преимущества синхронизации, описанные в предыдущей эвристике. Ты может даже разделить приложение аналогично потокам путем создания невидимого окна для каждого сокета. Если у вас есть два разные типы розеток, каждая розетка может иметь свои уведомления отправлено в окно другого типа. Говоря простым языком API, это означает отдельный WndProc () для каждого типа сокета.С точки зрения такие фреймворки, как MFC, вы можете поместить код для каждого типа сокета в другой подкласс CWnd .

Эвристика 10: используйте потоки только тогда, когда они влияют на остальная часть программы легко сдерживается.

Предыдущий эвристический указатель предупреждает, что потоки часто бывают очень сложными. правильно программировать, но правда в том, что они иногда очень полезно. Вы можете сделать обоснованное предположение о том, будут ли потоки улучшить программу, немного поработав над дизайном: есть ли чистый интерфейс между каждым потоком и остальной частью программы? Если так, синхронизация становится простой.Если нет, ты закончишь с беспорядком, который приводит к сбою и непредсказуемому уничтожению данных.

Примеры жизнеспособности потоков:

  1. FTP-сервер. Один из способов записать FTP-сервер — это пусть основной поток принимает входящие сетевые подключения, и отправьте каждый в отдельный поток. Затем каждый поток может обрабатывать входящие команды FTP, отправлять требуемые ответы, и завершиться при закрытии сеанса. Потому что каждый поток никогда должен взаимодействовать с любым другим, и все они действуют одинаково, это идеальное применение ниток.(Но помните предыдущий эвристика, связанная с сервером: один поток на клиента сильно ограничивает масштабируемость вашего сервера.)
  2. Веб-браузер. При загрузке файла с современной веб-браузере, файл откроется в фоновом режиме, так что вы может продолжить просмотр. Этот поток загрузки, скорее всего, обработан в специальной ветке.
  3. Почтовая программа. В почтовой программе основной обычно основное внимание уделяется чтению и написанию электронной почты. Однако когда сообщение электронной почты нужно отправить, лучше не прерывать работа пользователя.Вы можете отправить это сообщение с отдельным сетевой поток, так как процесс влияет на остальную часть программы только минимально.
  4. Биржевой тикер. Сведено к основам, биржевой код просто отображает небольшой объем непрерывных данных в реальном времени в приятном и полезный формат. Когда объем задействованных сетевых данных невелик, накладные расходы на синхронизацию потоков становятся незначительными. Плюс, такое приложение имеет только одну структуру данных, которая нуждается в защите; появляются действительно большие проблемы с синхронизацией когда необходимо защитить несколько структур данных.
Эвристика 11: проектируйте с учетом вашего протокола.

Некоторые сетевые протоколы по своей природе синхронны, а другие не. Примером синхронного протокола является протокол электронной почты POP3: отправить имя пользователя, получить ответ, отправить пароль, получить ответ, отправить запрос на получение списка писем, получить ответ … С помощью POP, вы должны отправлять эти команды в определенном порядке: вы не можете отправьте пароль перед именем пользователя, и вы не сможете получить список писем без отправки имени пользователя и пароля.Письмо клиенту POP с несинхронным типом сокета также потребуется написание конечного автомата.

С другой стороны, если ваш протокол не синхронный, вы можете также используйте несинхронные сокеты. Несинхронные протоколы имеют тенденцию напоминать набор вызовов функций. Рассмотрим, например, программу для получения данных из сетевой базы данных SQL: отправить инструкцию SQL, и получить набор результатов. В конце каждого «вызова функции» программа вернулась в исходное состояние: вам не нужно поддерживать конечный автомат, чтобы отслеживать, где вы находитесь в протоколе.

Эвристика 12: Блокировать сокеты проще, неблокирующие сокеты более мощные.

Эта эвристика — почти повторение всего вышеперечисленного. Это просто стоит повторить это, в то время как блокирующие сокеты привлекательны для их простота, вы можете обнаружить, что в конечном итоге их недостатки вынудить вас перепроектировать вашу программу, чтобы использовать некоторую форму неблокирующей Розетки. Это особенно верно, если ваша программа будет поддерживать более одной розетки. (Виртуально все серверные программы попадают в эту категория.) Единственный разумный способ использовать несколько блокирующих сокетов сразу — использовать потоки, но с неблокирующими сокетами у вас есть еще много вариантов дизайна.