6.8 KiB
Лабораторная работа 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):
fetch_async(item, delay)— корутина:await asyncio.sleep(delay), вернуть(item, item * item).fetch_all_async(items, delay)— конкурентно все items запросов черезasyncio.gather. Вернуть(результаты, сек), упорядочены по item.fetch_all_limited(items, delay, max_concurrent)— то же, но не более max_concurrent одновременно (asyncio.Semaphore).measure_blocking_mistake(items, delay)— «ошибка новичка»: корутина используетtime.sleep(delay)вместоasyncio.sleep. Вернуть время выполнения: оно окажется ≈ items × delay — блокирующий сон парализует цикл событий.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 убил конкурентность.
Контрольные вопросы
- Чем кооперативная многозадачность отличается от вытесняющей?
- Почему между двумя
awaitкорутина «владеет» данными без Lock? - Что случится, если в корутине вызвать
time.sleep(10)? Проверьте. - Когда выбрали бы потоки, а когда asyncio? Приведите по примеру.
- Что делает
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 работы, тут это фича).