Files
parallel-programming/lab06_asyncio/методичка.md
T
2026-10-07 13:06:47 +05:00

6.8 KiB
Raw Blame History

Лабораторная работа 6. Асинхронный код (asyncio)

Время на работу: 2 пары. Зависимости: лабы 1–2.

Цель работы

Освоить кооперативную многозадачность: async/await, event loop, gather, ограничение конкурентности семафором. Понять, чем asyncio отличается от потоков и когда выбирать её.

Теория

Модель: один поток, но тысячи задач

Asyncio — однопоточная кооперативная многозадачность. Есть один цикл событий (event loop) и множество корутин. Корутина выполняется, пока сама не уступит управление через await — это ключевое отличие от потоков, где ОС переключает задачи принудительно в любой момент (вытесняющая многозадачность).

Следствия:

  • нет гонок на counter += 1 между await-ами: код между await-ами исполняется атомарно (переключение — только на await);
  • зато «жадная» корутина без await блокирует всё;
  • одна корутина может ожидать тысячи сокетов — накладные расходы на задачу в разы меньше, чем на поток.

Ключевые слова и функции

async def fetch(i):           # корутина — «ленивая» функция
    await asyncio.sleep(0.05) # уступает управление на время ожидания
    return i

async def main():
    results = await asyncio.gather(             # запустить все и ждать все
        fetch(1), fetch(2), fetch(3))
    task = asyncio.create_task(fetch(4))        # задача: стартовала сейчас
    r = await task                              # ...а результат ждём тут

asyncio.run(main()) — вход в async-мир из обычного кода (один раз на программу). await можно ставить только внутри async def.

gather и семафор

gather запускает все корутины конкурентно. Если задач тысячи, а «сервер» выдерживает 100 соединений — используем asyncio.Semaphore:

sem = asyncio.Semaphore(100)

async def limited(i):
    async with sem:
        return await fetch(i)

Потоки или asyncio?

Потоки asyncio
Переключение вытесняющее (ОС) кооперативное (await)
Масштаб задач десятки–сотни тысячи и больше
Гонки данных да только между await-ами
CPU-код бесполезен (GIL) блокирует всё
Блокирующие библиотеки подходят блокируют цикл (!)

Критерий: много I/O + нужно много одновременных задач → asyncio. CPU-работа → процессы (лаба 4). Смешанная → комбинировать.

Чем опасна блокирующая функция в async-коде

time.sleep(0.05) (не asyncio.sleep) внутри корутины останавливает ВЕСЬ цикл событий: ни одна другая корутина не выполняется. В работе это демонстрируется явно.

Задание

Заполните solution.py (все функции async, кроме main):

  1. fetch_async(item, delay) — корутина: await asyncio.sleep(delay), вернуть (item, item * item).
  2. fetch_all_async(items, delay) — конкурентно все items запросов через asyncio.gather. Вернуть (результаты, сек), упорядочены по item.
  3. fetch_all_limited(items, delay, max_concurrent) — то же, но не более max_concurrent одновременно (asyncio.Semaphore).
  4. measure_blocking_mistake(items, delay) — «ошибка новичка»: корутина использует time.sleep(delay) вместо asyncio.sleep. Вернуть время выполнения: оно окажется ≈ items × delay — блокирующий сон парализует цикл событий.
  5. main() — все пункты + сравнение с потоками (лаба 2).

Ожидаемые результаты

  • fetch_all_async(96, 0.05) ≈ 0.05–0.1 c — все запросы «одновременно».
  • fetch_all_limited(96, 0.05, 8) ≈ 0.6 c.
  • measure_blocking_mistake(96, 0.05) ≈ 4.8 c — блокирующий sleep убил конкурентность.

Контрольные вопросы

  1. Чем кооперативная многозадачность отличается от вытесняющей?
  2. Почему между двумя await корутина «владеет» данными без Lock?
  3. Что случится, если в корутине вызвать time.sleep(10)? Проверьте.
  4. Когда выбрали бы потоки, а когда asyncio? Приведите по примеру.
  5. Что делает asyncio.gather и чем отличается от последовательных await?

Что сдаётся

solution.py + отчёт: таблица сравнения (последовательно/потоки/async/ async+лимит/ошибка с time.sleep), анализ, ответы на вопросы.

Критерии оценки

Пункт Баллы
fetch_all_async: время ≈ delay, результаты упорядочены 2
fetch_all_limited: лимит соблюдается 2
measure_blocking_mistake: продемонстрирована деградация 2
Сравнительная таблица в отчёте 2
Ответы на контрольные вопросы 2

Типичные ошибки

  • await вне async def — SyntaxError.
  • Забыт await перед корутиной: RuntimeWarning: never awaited, ничего не выполняется.
  • asyncio.run() внутри работающего цикла (Jupyter) — RuntimeError.
  • time.sleep в корутине — «секретный» последовательный режим (это п.4 работы, тут это фича).