Предметна область тут інша, ніж у лекції, — навмисно. У практиці ми «завантажували звіти», тут працюємо з чергою листів для розсилки: на кожен лист поштовий сервіс відповідає із затримкою, і затримки різні.
Мережу не використовуємо взагалі. Кожне звернення до сервісу імітуємо через
asyncio.sleep — для механіки циклу подій різниці немає жодної, а завдання не
залежить від інтернету й не турбує чужі сервери.
Усе роби в окремому файлі rozsylka.py або в новому зошиті — практику не редагуй,
вона знадобиться для звірки.
Якщо працюєш у зошиті.
asyncio.run()там впаде зRuntimeError— пишиawaitпрямо в клітинці. Якщо працюєш у файлі.py— навпаки,awaitна верхньому рівні не можна, загортай усе вasync def головна()і запускай черезasyncio.run(головна()). Обидва способи розібрані в практиці.
рівень 1Рівень 1 — База
Зміряй секундоміром різницю між послідовним очікуванням і asyncio.gather.
- Напиши корутину
надіслати(адреса, затримка): чекаєзатримкасекунд і повертає рядокf"{адреса}: доставлено". - Зроби список із п'яти адрес із затримками
0.1, 0.2, 0.3, 0.4, 0.5. - Надішли їх послідовно — циклом із
awaitусередині — і заміряй час черезtime.perf_counter(). - Надішли ті самі п'ять через
asyncio.gatherі заміряй час знову. - Надрукуй обидва числа й відношення одного до одного.
Зроблено, якщо:
- проходить
assert 1.5 <= час_послідовно < 2.0— тобто послідовний варіант справді дав суму затримок; - проходить
assert 0.5 <= час_разом < 0.9— тобтоgatherдав найдовшу затримку, а не суму; - проходить
assert результати == [f"{а}: доставлено" for а in адреси]— порядок у списку відgatherзбігається з порядком аргументів, а не з порядком доставки; - у коді є коментар, який пояснює, чому ці два числа відрізняються саме так.
рівень 2Рівень 2 — Плюс
Додай помилки й перепиши все на сучасний спосіб.
- Перепиши запуск із
gatherнаasync with asyncio.TaskGroup(). Результати бери з обʼєктів задач через.result()після виходу з блоку. - Додай шосту адресу
"поштова-скринька-переповнена"із затримкою0.1, яка кидаєConnectionError("скринька переповнена"). - Зроби так, щоб кожна успішна задача перед
returnдописувала свою адресу в списокдоставлені. - Злови
ExceptionGroupчерезexcept*і надрукуй: скільки задач упало, скільки встигло дійти до кінця й скільки часу протривала група. - Зроби другий варіант того самого — через
asyncio.gather(..., return_exceptions=True)— і надрукуй, чим два списки відрізняються.
Зроблено, якщо:
- проходить
assert len(спіймані) == 1 and isinstance(спіймані[0], ConnectionError); - проходить
assert доставлені == []— здорові задачі скасували, а не дочекались; - проходить
assert час_групи < 0.4— група вийшла одразу після падіння, а не через 0,5 секунди; - для варіанта з
return_exceptions=Trueпроходитьassert len(вдалі) == 5— тобто тут, навпаки, нікого не скасували й пʼять листів дійшли; - одним реченням у коментарі сформульовано, коли яка з двох поведінок доречніша.
рівень 3Рівень 3 — Виклик
Знайди межу застосовності числами, а не словами.
- Побудуй табличку «кількість задач → загальний час» для 1, 2, 4, 8, 16, 32, 128
і 512 задач, кожна із затримкою
0.1секунди. Для кожного рядка порахуй накладні витрати:вимір − 0.1. - Зроби другу таку саму табличку, де замість
await asyncio.sleep(0.1)кожна задача виконує обчислення приблизно на ті самі 0,1 секунди (наприклад, суму квадратів у циклі — підбери межу під свою машину). - Постав дослід із
time.sleep(0.1)замістьasyncio.sleep(0.1)для 8 задач і заміряй час. - Зміряй, скільки триває варіант із
time.sleep, загорнутий уasyncio.to_thread. - Напиши висновок на пʼять речень: за яких умов асинхронність варта ускладнення коду, а за яких ні. Спирайся на свої числа, а не на числа з лекції.
Зроблено, якщо:
- у першій табличці час для 512 задач менший за 0,5 секунди, а накладні витрати виміряні, а не переписані звідкись;
- у другій табличці видно, що час росте лінійно з кількістю задач, і в коментарі
пояснено, чому
gatherтут не допоміг; - проходить
assert час_з_time_sleep > 5 * час_з_asyncio_sleepдля восьми задач; - проходить
assert час_через_to_thread < час_з_time_sleep / 2; - у висновку названо конкретне число задач, після якого виграш стає помітним на твоїй машині, і сказано, звідки це число взялось.
Підказки
- Рівень 1. Якщо
gatherдав ту саму суму, що й послідовний варіант, — швидше за все, ти передав у нього вже готові результати, а не корутини. Перевір: усередині дужокgatherмають стояти викликинадіслати(...)безawait. - Рівень 2.
except*— це окремий синтаксис, не описка. Він приймає групу винятків і віддає меншу групу; сам виняток лежить упачка.exceptions. Гілокexcept*може спрацювати кілька, якщо в групі різні типи помилок. - Рівень 2. Щоб побачити скасування на власні очі, а не повірити на слово, зроби здорові задачі довшими за поламану. Якщо вони коротші, вони встигнуть завершитись самі, і скасовувати буде нікого.
- Рівень 3. Для 512 задач вивід «почав/готово» перетвориться на стіну тексту —
прибери
printіз самої корутини й друкуй тільки підсумок по кожному рядку таблиці. - Рівень 3. Якщо накладні витрати вийшли підозріло великими, перевір, чи не
вимірюєш ти заразом і створення корутин. Секундомір має вмикатись безпосередньо
перед
await asyncio.gather(...). - Загальна. Різні затримки зручніше задавати списком і збирати корутини циклом
у звичайний список, а потім розпаковувати:
await asyncio.gather(*список).