Перейти к основному содержимому

Асинхронное программирование

📺 Слайды к лекции

Материалы в разработке

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

Корутины и async/await

Корутина — функция, помеченная async def. Она может приостанавливать своё выполнение с помощью await:

coroutine.py
import asyncio

async def fetch_data(url: str) -> str:
print(f"Начинаем загрузку {url}")
await asyncio.sleep(1) # имитация I/O
print(f"Загрузка {url} завершена")
return f"data from {url}"

async def main():
result = await fetch_data("https://example.com")
print(result)

asyncio.run(main())
Определение

Корутина — это обобщение генератора. Генераторы используют yield для ленивых вычислений, корутины используют await для приостановки до завершения асинхронной операции.

Event Loop — цикл событий

Event loop — центральный компонент asyncio. Он управляет выполнением корутин, обработкой I/O-событий и планированием задач.

Порядок работы:

  1. Инициализация событийного цикла
  2. Извлечение задачи из очереди
  3. Выполнение задачи или приостановка до готовности I/O
  4. Обработка завершённых операций
  5. Переход к следующей задаче
  6. Повтор до завершения всех задач
Важно

Event loop работает в одном потоке. Блокирующие вызовы (например, time.sleep(), requests.get()) замораживают весь цикл. Используйте только асинхронные аналоги.

asyncio.run и asyncio.create_task

tasks.py
import asyncio

async def download(url: str, delay: float) -> str:
await asyncio.sleep(delay)
return f"Downloaded {url}"

async def main():
# Последовательно — медленно
r1 = await download("url1", 1)
r2 = await download("url2", 1)
# Итого: ~2 секунды

# Параллельно — быстро
task1 = asyncio.create_task(download("url1", 1))
task2 = asyncio.create_task(download("url2", 1))
r1 = await task1
r2 = await task2
# Итого: ~1 секунда

asyncio.run(main())

asyncio.gather и asyncio.wait

gather.py
import asyncio

async def fetch(n):
await asyncio.sleep(n)
return f"result-{n}"

async def main():
# gather — запуск нескольких корутин, ожидание всех
results = await asyncio.gather(
fetch(1), fetch(2), fetch(3)
)
print(results) # ['result-1', 'result-2', 'result-3']

# wait — больше контроля
tasks = {asyncio.create_task(fetch(i)) for i in range(3)}
done, pending = await asyncio.wait(tasks, timeout=2)
for task in done:
print(task.result())

asyncio.run(main())

aiohttp — асинхронные HTTP-запросы

aiohttp_example.py
import aiohttp
import asyncio

async def fetch_url(session, url):
async with session.get(url) as response:
return await response.text()

async def main():
urls = [
"https://python.org",
"https://docs.python.org",
"https://pypi.org",
]
async with aiohttp.ClientSession() as session:
tasks = [fetch_url(session, url) for url in urls]
results = await asyncio.gather(*tasks)
for url, html in zip(urls, results):
print(f"{url}: {len(html)} chars")

asyncio.run(main())
Частая ошибка

Не используйте requests в асинхронном коде — он блокирует event loop. Используйте aiohttp или httpx с async-API.

Асинхронные контекстные менеджеры

async_context.py
class AsyncResource:
async def __aenter__(self):
print("Acquiring resource")
await asyncio.sleep(0.1)
return self

async def __aexit__(self, exc_type, exc_val, exc_tb):
print("Releasing resource")
await asyncio.sleep(0.1)

async def main():
async with AsyncResource() as resource:
print("Using resource")

Асинхронные генераторы

async_gen.py
import asyncio

async def async_range(n):
for i in range(n):
await asyncio.sleep(0.1)
yield i

async def main():
async for value in async_range(5):
print(value)

asyncio.run(main())

Синхронизация в asyncio

async_sync.py
import asyncio

# Lock — эксклюзивный доступ
lock = asyncio.Lock()
async def critical_section():
async with lock:
await asyncio.sleep(1) # только одна корутина здесь

# Semaphore — ограничение параллелизма
sem = asyncio.Semaphore(3)
async def limited_task():
async with sem:
await asyncio.sleep(1) # макс. 3 одновременно

# Event — сигнализация
event = asyncio.Event()
async def waiter():
await event.wait()
print("Event fired!")
Практический совет

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

Отличия от потоков и процессов

Критерийasynciothreadingmultiprocessing
ПереключениеКооперативное (await)Вытесняющее (GIL)Вытесняющее (ОС)
GILНе применимОграничивает CPUСвой GIL на процесс
Накладные расходыМинимальныеСредниеВысокие
Лучше дляI/O-bound (много соединений)I/O-bound (немного потоков)CPU-bound
Race conditionsРедки (await-точки)ЧастыеНет (изоляция)