# Лабораторная работа 6. Асинхронный код (asyncio) **Время на работу:** 2 пары. **Зависимости:** лабы 1–2. ## Цель работы Освоить кооперативную многозадачность: `async/await`, event loop, `gather`, ограничение конкурентности семафором. Понять, чем asyncio отличается от потоков и когда выбирать её. ## Теория ### Модель: один поток, но тысячи задач Asyncio — **однопоточная** кооперативная многозадачность. Есть один цикл событий (event loop) и множество корутин. Корутина выполняется, пока сама не уступит управление через `await` — это ключевое отличие от потоков, где ОС переключает задачи принудительно в любой момент (вытесняющая многозадачность). Следствия: - нет гонок на `counter += 1` между await-ами: код между await-ами исполняется атомарно (переключение — только на await); - зато «жадная» корутина без await блокирует всё; - одна корутина может ожидать тысячи сокетов — накладные расходы на задачу в разы меньше, чем на поток. ### Ключевые слова и функции ```python 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`: ```python 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 работы, тут это фича).