Comments 1
Спасибо за перевод статьи, но часть с «введением» кажется немного недожатой. То есть, есть примеры с аналогииями на уровне «для начинающих», но для этих же начинающих имхо маловато конкретики, плюс отсутствуют недостатки.
Так что попробую дополнить:
В целом, реализовывать асинхронный подход можно несколькими способами:
1. При отсутствии поддержки языка — мы можем организовать некоторый список задач, которые, например, неблокирующе читают содержимое сокетов, дописывают в свою структуру новые данные, парсят и при окончательном парсинге — выдают результат (например, как здесь.). В данном случае, у нас получается довольно запутанный код, так как нам приходится учитывать текущее состояние, в случае HTTP — состояние «мы ждём и парсим заголовки» или, если в заголовках было тело — «мы принимаем тело ответа».
Таким образом мы вполне себе асинхронно и без каких либо средств языка решаем задачи обработки сразу множества клиентов, нахально эксплуатируя, например, неблокирующие сокеты, досрочно выходя из функции/метода пока она не будет закончена полностью, и удерживая состояние где-то снаружи. Фактически, это конструирование конечных автоматов, которые переключаются между состояниями по определённым событиям, и мы их дёргаем пока те не придут к какому-то определённому состоянию окончания работы.
2. Если есть поддержка со стороны языка — мы можем использовать корутины.
Корутина — это такая функция, которая фактически может поставить себя на паузу, выдав какой-то промежуточный результат (или приняв, или вовсе ничего не сделать и просто выдать себя назад), и её можно будет вызвать снова, с того же места где её прервали, учитывая все промежуточные переменные, области видимости и состояние функции в целом. Что позволяет писать асинхронный код так, как будто он синхронный:
В чём разница между двумя подходами?
— Первый — универсален, но в нём легко запутаться и сложно поддерживать, особенно при множестве состояний. Функция выглядит как множество goto, от начала до разных точек if-блоков (технически являющихся goto-метками).
— Второй — сложнее в общем понимании процесса (необходимо разбираться с yield'ами и корутинами, писать для них обёртки) и медленнее, ибо генераторы в принципе более медленные чем функции (требуется сохранять/восстанавливать стек), но с их помощью, асинхронный код в целом выглядит прямым.
Так что попробую дополнить:
В целом, реализовывать асинхронный подход можно несколькими способами:
1. При отсутствии поддержки языка — мы можем организовать некоторый список задач, которые, например, неблокирующе читают содержимое сокетов, дописывают в свою структуру новые данные, парсят и при окончательном парсинге — выдают результат (например, как здесь.). В данном случае, у нас получается довольно запутанный код, так как нам приходится учитывать текущее состояние, в случае HTTP — состояние «мы ждём и парсим заголовки» или, если в заголовках было тело — «мы принимаем тело ответа».
# вызываем эту функцию много раз, пока принимаем http-данные с принятым кусочком
# состояние хранится в каком-нибудь объекте-таске
def append_data_chunk(chunk):
if self.state = "recv_headers":
self.data += chunk
if "\r\n\r\n" in self.data:
self.body, self.body_len = self.parse_headers(self.data)
if self.body and self.body_len > 0:
self.state = "recv_body"
if self.state = "recv_body":
self.body += chunk
if len(self.body) == self.body_len:
return self.headers, self.body
# ...
Таким образом мы вполне себе асинхронно и без каких либо средств языка решаем задачи обработки сразу множества клиентов, нахально эксплуатируя, например, неблокирующие сокеты, досрочно выходя из функции/метода пока она не будет закончена полностью, и удерживая состояние где-то снаружи. Фактически, это конструирование конечных автоматов, которые переключаются между состояниями по определённым событиям, и мы их дёргаем пока те не придут к какому-то определённому состоянию окончания работы.
2. Если есть поддержка со стороны языка — мы можем использовать корутины.
Корутина — это такая функция, которая фактически может поставить себя на паузу, выдав какой-то промежуточный результат (или приняв, или вовсе ничего не сделать и просто выдать себя назад), и её можно будет вызвать снова, с того же места где её прервали, учитывая все промежуточные переменные, области видимости и состояние функции в целом. Что позволяет писать асинхронный код так, как будто он синхронный:
# получаем генератор, и вызываем его много раз подряд,
# отправляя новые кусочки принятых данных,
# состояние хранится прямо внутри функции
def deal_with_request():
headerstring = ""
# ждём, пока при очередном вызове корутино-генератора
# не придёт хвост http-запроса
while not "\r\n\r\n" in headerstring:
headerstring += yield
headers, bodystring = parse_headers(headerstring)
len = headers.get("Content-Length", 0)
# мы можем вернуть заголовки при отсутствии тела
if len == 0:
return headers, bodystring
# или продолжить требовать отправлять нам кусочки
# пока не будет принято тело сообщения
while len(bodystring) < len:
bodystring += yield
return headers, bodystring
# ... здесь должен быть шедулер, делающий грязную работу вызовов корутино-генераторов
# но хоть код основной логики выглядит более-менее прямым
В чём разница между двумя подходами?
— Первый — универсален, но в нём легко запутаться и сложно поддерживать, особенно при множестве состояний. Функция выглядит как множество goto, от начала до разных точек if-блоков (технически являющихся goto-метками).
— Второй — сложнее в общем понимании процесса (необходимо разбираться с yield'ами и корутинами, писать для них обёртки) и медленнее, ибо генераторы в принципе более медленные чем функции (требуется сохранять/восстанавливать стек), но с их помощью, асинхронный код в целом выглядит прямым.
+2
Sign up to leave a comment.
Введение в асинхронное программирование на Python