Запуск тестового проекта ^
python3 run.py
Затем нужно открыть в браузере http://0.0.0.0:8000, далее переходить по ссылкам. В дебаг-режиме будет доступна временнáя диаграмма, где темно-зеленым цветом обозначены асинхронные http-походы в соответствии со временем выполнения.
на скришоте дебаг-страница с 4-мя последовательными запросами одинаковой длительности
Добавление своих примеров ^
Проект содержит страницу для выполнения примеров запросов 0.0.0.0:8000/example_request?duration=1
,
где duration - продолжительность запроса в секундах. Хендлер
example_request.py
содержит неблокирующий вызов asyncio.sleep()
.
http-клиент фронтика ^
При использовании современных http-клиентов либо библиотек для работы с базами обычно подобные методы
возвращают корутины. При создании корутины-объекта из корутины-функции (вызов coro()
) задача не ставится на выполнение сама -
для этого мы должны заавейтить ее либо создать таску.
Давайте разберемся, что же представляют собой методы get_url
, post_url
, put_url
и delete_url
из фронтика.
- Под капотом они пока еще реализованы на старых торнадовских коллбеках, и при вызове любого метода http-клиента запрос сразу же ставится на выполнение. Это аналог современных тасок.
- Метод возвращает
asyncio.Future
, поэтому мы можем await'ить эту фьючу изAwaitablePageHandler
и yield'ить ее же изPageHandler
, дожидаясь результатов запроса для последующей обработки. Фьюча является тем костылем по сути, который связывает коллбеки и корутины, нативные и торнадовские. Теория
future = handler.get_url(
some_host,
'/path/to/resource',
data=data,
headers=headers,
)
result = await future
В коде старых проектов вместо yield
либо await
используется аргумент callback
(чтобы обработать результат запроса),
который не рекомендуется в современном коде.
def cb(xml, response):
if xml is None or response.error:
raise HTTPError(response.code)
handler.doc.put(xml)
handler.get_url(
some_host,
'/path/to/resource',
data=data,
headers=headers,
callback=cb,
)
Если при вызове http-метода не указывать флаг waited=False
, то фьюча
добавится
в handler.finish_group
и
гарантированно выполнится
до отдачи ответа.
Паттерны ^
Нативные корутины async def
^
Доступны только внутри AwaitablePageHandler
. Все его http-методы должны быть
нативными корутинами.
Внутри этих методов доступны все возможности современного асинхронного питона - походы в базы,
кеши, очереди и многое другое. Это самый лучший вариант, хорошо оптимизированный на уровне интерпретатора.
Последовательное выполнение ^
[ debug page, code ]
Делается ключевым словом await
- код ниже этой инструкции продолжит выполняться только после завершения работы запроса/корутины.
await self.get_url(
some_host,
'/request1',
)
await self.get_url(
some_host,
'/request2',
)
Можно авейтить как сам запрос self.get_url()
, так и корутину, делающую запрос и как-то обрабатывающую его результат.
[ debug page, code ]
async def get_result(handler):
result = await handler.get_url(
some_host,
'/request2',
)
if result.failed:
return ''
return result.data['key']
key = await get_result(self)
Параллельное выполнениее ^
[ debug page, code ]
Делается при помощи asyncio.gather()
, который принимает аргументы через запятую.
result1, result2 = await asyncio.gather(
self.get_url(
some_host,
'/request1',
),
self.get_url(
some_host,
'/request2',
)
)
Если нужно в метод передать массив фьюч/корутин, то его надо раскрыть. В переменной results будет содержаться массив с результатами, по количеству фьюч.
futures = [future1, future2]
results = asyncio.gather(*futures)
Можно использовать в asyncio.gather()
как http-методы, которые возвращают фьючи, так и корутины.
Иногда удобно использовать словарь, сопоставляя ключ и фьючи, как это было в торнадо. Для этого дабавлен метод gather_dict
.
[ debug page, code ]
from frontik.util import gather_dict
coro_dict = {
'result1': self.get_url(
some_host,
'/request1',
),
'result2': self.get_url(
some_host,
'/request2',
),
}
result = await gather_dict(coro_dict=coro_dict)
key = result['result1'].data['key']
Независимая таска ^
Выполняется "параллельно" основному коду, не блокирует его, если ее явно не авейтить).
Будет добавляться в self.finish_group и авейтиться с ней перед отдачей ответа.
Может быть полезна в сложном коде, чтобы вынести какие-то независимые задачи из основного кода,
а также если надо вначале запустить задачу, а заавейтить где-то позже в коде.
По сути, воспроизводит поведение [http-поход+обработка в коллбеке] в старом коде.
Задачи должны быть действительно независимыми, чтобы не повторять ситуации гонки в коллбечном коде.
Реализована за счет asyncio.create_task(coro())
.
! Таску можно создать только из корутины, но из http-метода, который возвращает фьючу - нет !
[ debug page, code ]
async def put_result(handler):
result = await handler.get_url(
some_host,
'/request2',
)
if result.failed:
return
handler.doc.put(result.data)
return result.data
task = self.run_task(put_result(self))
# .... заавейтить можно позже; в любом случае заавейтится в finish_group
result = await task
Корутины торнадо @gen.coroutine
[deprecated] ^
Все http-методы обычного PageHandler
являются корутинами торнадо, так как они
декорируются
@gen.coroutine
в коде фронтика.
Начиная с 5 версии, в торнадо под капотом стали использоваться нативные возможности асинхронного питона,
в первую очередь event loop и Future из asyncio. По сути, корутины торнадо являются костылями к ним, что
приводит к большому стеку вызовов и снижению производительности по сравнению с нативными корутинами.
Последовательное выполнение ^
[ debug page, code ]
Делается ключевым словом yield
- код ниже этой инструкции продолжит выполняться только после завершения работы запроса/корутины.
yield self.get_url(
some_host,
'/request1',
)
yield self.get_url(
some_host,
'/request2',
)
Можно yield-ить как сам запрос self.get_url()
, так и корутину, делающую запрос и как-то обрабатывающую его результат.
[ debug page, code ]
@gen.coroutine
def get_result(handler):
result = yield handler.get_url(
some_host,
'/request2',
)
if result.failed:
return ''
return result.data['key']
key = yield get_result(self)
Параллельное выполнениее ^
[ debug page, code ]
Делается при помощи все того же yield, передавая ему массив корутин. Данные для него под капотом оборачиваются в gen.multi
.
result1, result2 = yield [
self.get_url(
some_host,
'/request1',
),
self.get_url(
some_host,
'/request2',
)
]
С yield
для параллельного выполнения также можно использовать словарь, сопоставляя ключ и фьючи - иногда это бывает удобно.
[ debug page, code ]
results = yield {
'result1': self.get_url(
some_host,
'/request1',
),
'result2': self.get_url(
some_host,
'/request2',
),
}
key = result['result1'].data['key']
Независимая таска ^
Выполняется "параллельно" основному коду, не блокирует его, если ее явно не авейтить). Будет добавляться в self.finish_group и авейтиться с ней перед отдачей ответа. Может быть полезна в сложном коде, чтобы вынести какие-то независимые задачи из основного кода, а также если надо вначале запустить задачу, а за-yield'ить где-то позже в коде. По сути, воспроизводит поведение [http-поход+обработка в коллбеке] в старом коде.
! Таску нужно создавать только из корутины, так как сам по себе http-метод по сути является таской, которая запускается сразу же, и будет заавейчена в finish_group. Если в корутине есть запрос, он будет гарантированно выполнен до отдачи ответа, а вот обработка результата - не факт что успеет, поэтому такую корутину нужно вручную добавить в finish_group.
[ debug page, code ]
@gen.coroutine
def put_result(handler):
result = yield handler.get_url(
some_host,
'/request2',
)
if result.failed:
return
handler.doc.put(result.data)
return result.data
fut = get_result(self, 1.1)
self.finish_group.add_future(fut)
# .... за-yield'ить можно позже; в любом случае за-yield'ится в finish_group
result = yield task
Коллбеки торнадо [deprecated] ^
Коллбеки в версии 5.1.1
были задепрекейчены,
а в 6 выпилены - чтобы полностью соответствовать требованиям современной
асинхронной разработки на питоне - "в пользовательском коде должны быть только корутины и таски, а коллбеки и фьючи
использоваться только для низкоуровневых задач".
Для выполнения нескольких параллельных запросов и затем общего результирующего коллбека во фронтик-приложениях
использовалась AsyncGroup
Последовательное выполнение ^
[ debug page, code ]
В коллбек предыдущего запроса добавляется следующий запрос со своим коллбеком - и так далее.
def cb(data, response):
self.get_url(
'0.0.0.0:8000',
'/example_request',
data={'duration': 0.5},
callback=cb2,
)
self.get_url(
'0.0.0.0:8000',
'/example_request',
data={'duration': 0.5},
callback=cb,
)
Параллельное выполнениее ^
[ debug page, code ]
Делается при помощи AsyncGroup
,
которой передается финальный коллбек для данной группы, а каждому из вызовов http-методов - коллбек, обернутый в метод add
этой AsyncGroup'ы.
results = []
def finish_cb():
print(results)
def cb(json, response):
results.append(json)
async_group = AsyncGroup(finish_cb)
self.get_url(
some_host,
'/request1',
callback=async_group.add(cb),
)
self.get_url(
some_host,
'/request2',
callback=async_group.add(cb),
)
Независимая таска ^
Сам вызов http-метода с коллбеком или без него по сути является независимой таской, так как запускается сразу же, а его фьюча добавляется в finish_group.
! Иногда в старом коде (в новом это не рекомендуется) можно встретить вызов http-метода с коллбеком из торнадовской корутины. В этом случае задача, как обычно, запустится в месте вызова, когда придет время выполнения этой корутины, и заавейтится в finish_group.
[ debug page, code ]
def cb(json, response):
results.append(json)
self.get_url(
some_host,
'/request1',
callback=cb,
)