Асинхронное
программирование
Tinkoff Python
Лекция 8
Палий Антон
Виды соединений
Poll
Постоянно заваливаем запросами
+ простота реализации
- очень велики расходы на постоянную установку соединения
не рекомендуется к использованию
Poll
Long Polling
- Отправляется запрос на сервер
- Соединение не закрывается сервером, пока не появится событие или наступит таймаут
- Событие отправляется в ответ на запрос
- Клиент тут же отправляет новый ожидающий запрос
К примеру поможет для получения реалтайм оповещений
+ Простота реализаци
- Опять же необходимость каждый раз устанавливать соединение
- Нет обратной связи
Long Polling
WebSocket
Соединение, при котором и клиент и сервер могут обмениваться друг с другом данными
+ Создается одно соедиенение и в его рамках идет передача всех данных
+ Передача данных в обе стороны
- Более сложная реализация
WebSocket
c10k
Действительно ли проблема?
c10m
вот это проблема!
10 миллионов процессов?
import time
from concurrent.futures import ProcessPoolExecutor
import requests
# во всех примерах используется сервер, который отвечает за 0.3 секунды
def load_many() -> None:
session = requests.Session()
tasks = []
with ProcessPoolExecutor(max_workers=100) as pool:
for i in range(500):
tasks.append(pool.submit(session.get, "http://127.0.0.1:8000/"))
for task in tasks:
print(task.result().json())
start = time.monotonic()
load_many()
duration = time.monotonic() - start
print(f"spend {duration} seconds")
python process-load.py
...
spend 2.271605703019304 seconds
10 миллионов тредов?
import time
from concurrent.futures import ThreadPoolExecutor
import requests
def load_many() -> None:
session = requests.Session()
tasks = []
with ThreadPoolExecutor(max_workers=100) as pool:
for i in range(500):
tasks.append(pool.submit(session.get, "http://127.0.0.1/:8000"))
for task in tasks:
print(task.result().json())
start = time.monotonic()
load_many()
duration = time.monotonic() - start
print(f"spend {duration} seconds")
python thread-load.py
...
spend 2.1006762809993234 seconds
Слишком накладно?
Синхронизировать это все дело, следить чтобы ничего не утекло, правильно поставлены локи, потокобезоапасная сессии и т.д.
На каждое пользовательское соединение отдельный поток, процесс?
Чем больше тредов, тем дольше будет переключаться контекст в операционной системе.
Слишком жирно?
Каждый тред или процесс будут выжирать память и время cpu, при этом не использоваться на полную мощность
В синхронных операциях задачи выполняются друг за другом.
В асинхронных задачи могут запускаться и завершаться независимо друг от друга.
Такой подход позволяет решить проблему с ресурсами и скоростью работы кода
Асинхронное программирование
Это вызов асинхронных системных функций.
Но по факту сложнее.
Почему же?
Системные вызовы сложны
Мы же пишем на питоне
Есть разные шаблоны, которые упрощают работку с асинхронными вызовами
Event Loop
Очередь из задач, которая регулирует порядок их запуска в системе.
Callback
Функции обратного вызова
import tornado.ioloop
from tornado.httpclient import AsyncHTTPClient
def handle_response(response):
if response.error:
print("Error:", response.error)
else:
url = response.request.url
data = response.body
print("{}: {} bytes: {}".format(url, len(data), data))
def load_many():
http_client = AsyncHTTPClient()
tasks = []
for i in range(500):
tasks.append(http_client.fetch("http://127.0.0.1:8000/", handle_response))
tornado.ioloop.IOLoop.instance().start()
load_many()
very painful
Можно декомпозировать и написать иначе, но при написании кода на колбеках теряется линейность
Теория конечных автоматов
Конечный автомат — это некоторая абстрактная модель, содержащая конечное число состояний чего-либо, и регулирующая правило переключения этих состояний
Примитивы асинхронного кода
Корутина (сопрограмма)
Функция, которая может передавать управление циклу событий
In [1]: async def kek() -> None:
...: pass
...:
...: type(kek())
...:
/home/os/.local/bin/ipython:5: RuntimeWarning: coroutine 'kek' was never awaited
from IPython import start_ipython
RuntimeWarning: Enable tracemalloc to get the object allocation traceback
Out[1]: coroutine
In [2]: type(kek)
Out[2]: function
await
ключевое слово для получения результата корутины, или других объектов типа future или task
asyncio
основная библиотека для работы с асинхронностью
является частью стандартной библиотеки
Асинхронный код не запустить вне цикла событий
import asyncio
async def main():
pass
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
# Или просто
asyncio.run(main())
Task
Корутина запущенная concurrently
import asyncio
async def payload_work(num: int) -> None:
await asyncio.sleep(10)
print('kek')
async def main():
tasks = [asyncio.create_task(payload_work(i)) for i in range(10)]
asyncio.run(main())
Что выведет этот код?
Ничего, таска не успеет отработать
Правильно
import asyncio
async def payload_work(num: int) -> None:
await asyncio.sleep(10)
print('kek')
async def main():
tasks = [asyncio.create_task(payload_work(i)) for i in range(10)]
for task in tasks:
await tas
asyncio.run(main())
Что бы гарантировать результат выполнения из task, необходимо ее awaitнуть
Задачи можно отменять
import asyncio
async def payload_work(num: int) -> None:
try:
await asyncio.sleep(1)
print(f'kek {num}')
except Exception as e:
print(e)
async def main():
tasks = [asyncio.create_task(payload_work(i)) for i in range(10)]
tasks[5].cancel()
for task in tasks:
await task
asyncio.run(main())
# в данном случае все задачи отработают кроме 5
Полезно при получении ошибки в коде и задача больше не нужна
aiohttp
один из первых асинхронных веб фреймворков на основе asyncio
# Пример программы на aiohttp, клиент
import asyncio
import time
from typing import Dict
import aiohttp
async def load_one(session: aiohttp.ClientSession) -> Dict[str, str]:
async with session.get("http://127.0.0.1/") as result:
return await result.json()
async def load_many() -> None:
tasks = []
async with aiohttp.ClientSession() as session:
for i in range(500):
tasks.append(asyncio.create_task(load_one(session)))
for task in tasks:
res = await task
print(res)
start = time.monotonic()
asyncio.run(load_many())
duration = time.monotonic() - start
print(duration) # 1.767098006006563
# пример aiohttp веб сервера
from aiohttp import web
async def hello(request):
return web.Response(text="Hello, world")
app = web.Application()
app.add_routes([web.get("/", hello)])
web.run_app(app)
import asyncio
from fastapi import FastAPI
app = FastAPI()
# тот самый сервер на котором я тестировал
@app.get("/")
async def hello_world():
await asyncio.sleep(0.3)
return {"Hello": "World"}
import time
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def hello_world():
time.sleep(0.3)
return {"Hello": "World"}
Слегка пошутим
Результат стал хуже в 10 раз, хотя сервер запущен в 10 воркеров. асинхронная попытка выгрузить отработала за 18 секунд, в чем же дело?
Результат стал хуже в 10 раз, хотя сервер запущен в 10 воркеров. асинхронная попытка выгрузить отработала за 18 секунд, в чем же дело?
Вызывая синхронные блокирующие операции в асинхронном приложении, мы блокируем всё приложение.
А что если библиотека, которую мы используем не может в асинхронный код, но аналогов нету?
Text
from concurrent.fututers import ThreadPoolExecutor, ProcessPoolExecutor
await asyncio.run_in_executor(executor, function)
from functools import partial
await asyncio.run_in_executor(executor, partial(function, arg1, arg2))
import asyncio
import functools
import typing
def run_in_threadpool(func):
@functools.wraps(func)
def wrap(*args, **kwargs) -> typing.Awaitable[func]:
def inner():
return func(*args, **kwargs)
return asyncio.get_running_loop().run_in_executor(None, inner)
return wrap
@run_in_threadpool
def load_from_database():
...
return res
await load_from_database()
Множество библиотек имеют асинхронные реализации
aioredis
import asyncio
import aioredis
async def go():
redis = await aioredis.create_redis_pool(
'redis://localhost')
await redis.set('my-key', 'value')
val = await redis.get('my-key', encoding='utf-8')
print(val)
redis.close()
await redis.wait_closed()
asyncio.run(go())
# will print 'value'
pytest-asyncio
def test_http_client(event_loop):
url = 'http://httpbin.org/get'
resp = event_loop.run_until_complete(http_client(url))
assert b'HTTP/1.1 200 OK' in resp
@pytest.fixture()
async def async_fixture():
return await asyncio.sleep(0.1)
@pytest.mark.asyncio
async def test_some_asyncio_code():
res = await library.do_something()
assert b'expected result' == res
Errors
import asyncio
import time
from typing import Dict
import aiohttp
async def load_one(session: aiohttp.ClientSession) -> Dict[str, str]:
async with session.get("http://127.0.0.1/") as result:
return await result.json()
async def load_many() -> None:
tasks = []
async with aiohttp.ClientSession() as session:
for _ in range(10):
tasks.append(asyncio.create_task(load_one(session)))
start = time.monotonic()
asyncio.run(load_many())
duration = time.monotonic() - start
print(duration)
Пример 1
RuntimeError: Session is closed
Если задача использует контекст, необходимо проследить, что она выполнилась в рамках этого контекста
Вывод: ждем завершения задачи
Пример 2
import asyncio
async def payload_work(num: int) -> None:
await asyncio.sleep(1)
print(f'kek {num}')
async def main():
tasks = [asyncio.create_task(payload_work(i)) for i in range(10)]
tasks[5].cancel()
try:
for task in tasks:
await task
except asyncio.CancelledError:
raise
except Exception:
print('error')
asyncio.run(main())
asyncio.CancelledError
Операция была отменена.
В большинстве ситуаций необходимо прокидывать дальше.
Stop use except Exception
reraise CancelledError
Queue
import asyncio
import aiohttp
COUNT = 10
async def fetch(session: aiohttp.ClientSession, num: int, queue: asyncio.Queue) -> None:
async with session.get("https://official-joke-api.appspot.com/jokes/programming/random") as result:
await queue.put((num, await result.json()))
async def handle_results(queue: asyncio.Queue) -> None:
while True:
num, data = await queue.get()
print((num, data))
async def main() -> None:
queue = asyncio.Queue()
result_task = asyncio.create_task(handle_results(queue))
timeout = aiohttp.ClientTimeout(total=1.2)
async with aiohttp.ClientSession(timeout=timeout) as session:
tasks = []
for i in range(COUNT):
tasks.append(asyncio.create_task(fetch(session, i, queue)))
for task in tasks:
await task
await result_task
asyncio.run(main())
FastAPI
- pydantic
- openapi
- удобство
- скорость написания кода
- хвалебные отзывы
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
price: float
is_offer: bool = None
@app.get("/")
async def read_root():
return {"Hello": "World"}
@app.get("/items/{item_id}")
async def read_item(item_id: int, q: str = None):
return {"item_id": item_id, "q": q}
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
return {"item_name": item.name, "item_id": item_id}
Простой пример
-
Все данные провалидируются через pydantic
-
Автоматически сгенерируется сваггер
WebSocket
from fastapi import FastAPI, WebSocket
from fastapi.responses import HTMLResponse
app = FastAPI()
html = """
<!DOCTYPE html>
<html>
... код с js, который создает веб сокет с фронта
</html>
"""
@app.get("/")
async def get():
return HTMLResponse(html)
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
while True:
data = await websocket.receive_text()
await websocket.send_text(f"Message text was: {data}")
А если из кода?
import asyncio
import aiohttp
async def wc_connection():
async with aiohttp.ClientSession() as session:
async with session.ws_connect('http://127.0.0.1:8000/ws') as ws:
await ws.send_str('hi')
async for msg in ws:
await asyncio.sleep(1)
if msg.type == aiohttp.WSMsgType.TEXT:
if msg.data == 'close cmd':
await ws.close()
break
else:
await ws.send_str(msg.data)
print(msg.data)
elif msg.type == aiohttp.WSMsgType.ERROR:
break
asyncio.run(wc_connection())
Message text was: hi
Message text was: Message text was: hi
Message text was: Message text was: Message text was: hi
Message text was: Message text was: Message text was: Message text was: hi
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
price: float
is_offer: bool = None
@app.get("/")
def read_root():
return {"Hello": "World"}
@app.get("/items/{item_id}")
def read_item(item_id: int, q: str = None):
return {"item_id": item_id, "q": q}
@app.put("/items/{item_id}")
def update_item(item_id: int, item: Item):
return {"item_name": item.name, "item_id": item_id}
Можно писать и синхронные обработчики,
которые выполнятся в asyncio.run_in_executor
Тесты!
from fastapi import FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
@app.get("/")
async def read_main():
return {"msg": "Hello World"}
client = TestClient(app)
def test_read_main():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"msg": "Hello World"}
WSGI -> ASGI
ASGI - духовный насленик wsgi, но нацелен на асинхронность
uvicorn
Tinkoff Python 2020 - 8
By Afonasev Evgeniy
Tinkoff Python 2020 - 8
- 467