← Назад к вопросам

Как переключаются корутины в asyncio?

2.7 Senior🔥 91 комментариев
#Python Core#Асинхронность и многопоточность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как переключаются корутины в asyncio

Корутины в asyncio переключаются на основе события ввода-вывода (I/O events) и явных точек передачи контроля. Это кооперативная многозадачность (не вытесняющая), где каждая корутина добровольно отдаёт контроль другим, а не прерывается насильно.

Основной механизм: Event Loop

В asyncio всё управляется event loop (циклом событий) — это главный диспетчер, который:

  1. Держит очередь задач (корутин) готовых к выполнению
  2. Выполняет одну корутину за раз
  3. Когда корутина ждёт I/O (await), передаёт контроль другой
  4. Когда 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 завершена)

Ключевые моменты

  1. Кооперативность — корутины добровольно отдают контроль (await)
  2. Однопоточность — Event Loop выполняет в одном потоке
  3. I/O-bound — asyncio отлично подходит для сетевых операций
  4. Без await = блокировка — всегда используй await для I/O операций
  5. Порядок — непредсказуем, зависит от скорости I/O

Переключение корутин в asyncio — это эффективный способ обработки множества I/O операций в одном потоке, что критически важно для высокопроизводительных серверов и приложений.

Как переключаются корутины в asyncio? | PrepBro