← Назад к вопросам
Как переключаются корутины в asyncio?
2.7 Senior🔥 91 комментариев
#Python Core#Асинхронность и многопоточность
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Как переключаются корутины в asyncio
Корутины в asyncio переключаются на основе события ввода-вывода (I/O events) и явных точек передачи контроля. Это кооперативная многозадачность (не вытесняющая), где каждая корутина добровольно отдаёт контроль другим, а не прерывается насильно.
Основной механизм: Event Loop
В asyncio всё управляется event loop (циклом событий) — это главный диспетчер, который:
- Держит очередь задач (корутин) готовых к выполнению
- Выполняет одну корутину за раз
- Когда корутина ждёт I/O (await), передаёт контроль другой
- Когда I/O завершено, корутина возвращается в очередь
Event Loop управляет потоком:
Корутина A Корутина B Корутина C
┌──────────┐ ┌──────────┐ ┌──────────┐
│ работает│ │ в ожидании│ │ готова │
│ 10ms │ │ I/O │ │ запуск │
└──────────┘ └──────────┘ └──────────┘
↓ ↓ ↓
await (ждёт) Можем запустить!
↓ ↓
Отдаёт контроль ────────────→ Event Loop ─→ Запускает C
Точки переключения: await
Корутины переключаются только на await — это явное указание "я жду и готов отдать контроль".
import asyncio
import time
async def task_a():
print(f"A: начало {time.time():.2f}")
await asyncio.sleep(2) # ТОЧКА ПЕРЕКЛЮЧЕНИЯ
print(f"A: конец {time.time():.2f}")
async def task_b():
print(f"B: начало {time.time():.2f}")
await asyncio.sleep(1) # ТОЧКА ПЕРЕКЛЮЧЕНИЯ
print(f"B: конец {time.time():.2f}")
async def main():
# Запуск обеих корутин параллельно
await asyncio.gather(task_a(), task_b())
# Вывод (обе корутины работают одновременно, но в одном потоке!):
# A: начало 0.00
# B: начало 0.00
# B: конец 1.00 <- B завершена первой (её ждала меньше)
# A: конец 2.00 <- A завершена второй
asyncio.run(main())
Без await — блокирование
async def blocking_task_a():
print(f"A: начало {time.time():.2f}")
time.sleep(2) # БЕЗ await! БЛОКИРУЕТ весь event loop
print(f"A: конец {time.time():.2f}")
async def blocking_task_b():
print(f"B: начало {time.time():.2f}")
await asyncio.sleep(1)
print(f"B: конец {time.time():.2f}")
# Вывод:
# A: начало 0.00
# A: конец 2.00 <- Блокирует! B не может начать!
# B: начало 2.00
# B: конец 3.00 <- B начинается только после A
asyncio.run(asyncio.gather(blocking_task_a(), blocking_task_b()))
Как event loop выбирает, какую корутину запустить
# Event loop хранит три вида корутин:
1. READY (готовые к запуску)
2. WAITING (ждут I/O)
3. DONE (завершённые)
Цикл работает примерно так:
while not all_tasks_done:
# Получить все готовые корутины
ready_tasks = get_ready_tasks()
# Выполнить по одной
for task in ready_tasks:
try:
task.step() # Выполнить до следующего await
except StopIteration: # Корутина завершена
mark_as_done(task)
# Дождаться событий I/O
events = await_io_events(waiting_tasks, timeout=1.0)
# Добавить завершившиеся в READY
for event in events:
move_to_ready(event.task)
Пример с сетевыми запросами
import asyncio
import aiohttp
async def fetch_url(session, url: str) -> str:
async with session.get(url) as response:
# ↑ ТОЧКА ПЕРЕКЛЮЧЕНИЯ - ждём ответа от сервера
# Event loop может запустить другие корутины
text = await response.text()
return text
async def fetch_multiple(urls: list[str]) -> list[str]:
async with aiohttp.ClientSession() as session:
# Все запросы запускаются параллельно!
tasks = [fetch_url(session, url) for url in urls]
# ↑ Вместо последовательного ждания каждого запроса
results = await asyncio.gather(*tasks)
return results
# Если бы было sync (без asyncio):
# 3 запроса × 1 сек = 3 секунды
# С asyncio:
# 3 запроса параллельно = 1 секунда!
Порядок выполнения с задачами в очереди
async def a():
print("A1")
await asyncio.sleep(0) # Отдать контроль
print("A2")
async def b():
print("B1")
await asyncio.sleep(0)
print("B2")
async def main():
# Создаём задачи (они не начинают выполняться сразу!)
task_a = asyncio.create_task(a())
task_b = asyncio.create_task(b())
# Ждём обеих
await asyncio.gather(task_a, task_b)
# Вывод порядка:
# A1 <- Первая запущенная задача
# B1 <- Вторая запущенная задача
# A2 <- A завершилась (sleep(0) отдал контроль)
# B2 <- B завершилась
Важное: asyncio НЕ параллелен
# ❌ Неправильное предположение: asyncio = параллелизм
# На самом деле: asyncio = конкурентность в одном потоке
# Параллелизм (threading, multiprocessing):
# CPU: |──── Task A ────|──── Task B ────|
# Время: 0 1 2
# Несколько потоков одновременно
# Конкурентность (asyncio):
# CPU: |─ A ─||─ B ─||─ A ─||─ B ─|
# Время: 0 0.1 0.2 0.3 0.4
# Один поток, быстрое переключение
async def cpu_bound_task():
# Это БЛОКИРУЕТ весь event loop!
total = sum(range(1000000)) # ← Без await!
await asyncio.sleep(0)
# Для CPU-bound используй:
import concurrent.futures
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(
None, # использовать ThreadPoolExecutor по умолчанию
lambda: sum(range(1000000)) # ТОЧКА ПЕРЕКЛЮЧЕНИЯ!
)
Визуальное представление переключения
Время: 0ms 10ms 20ms 30ms
│ │ │ │
├─ A ────┤ │ │ (выполняется)
│ await│ │ │ (отдаёт контроль)
│ ├─ B ────┤ │ (выполняется)
│ │ await│ │ (отдаёт контроль)
│ │ ├─ C ────┤ (выполняется)
│ │ │ await│ (ждёт I/O)
│ ├─ A ────┤ │ (A готова - I/O готов)
│ │ ├─ B ────┤ (B готова)
│ │ │ ├─ C готова (I/O завершена)
Ключевые моменты
- Кооперативность — корутины добровольно отдают контроль (await)
- Однопоточность — Event Loop выполняет в одном потоке
- I/O-bound — asyncio отлично подходит для сетевых операций
- Без await = блокировка — всегда используй await для I/O операций
- Порядок — непредсказуем, зависит от скорости I/O
Переключение корутин в asyncio — это эффективный способ обработки множества I/O операций в одном потоке, что критически важно для высокопроизводительных серверов и приложений.