Асинхронное программирование – это стиль построения программ, при котором операции, потенциально длительно ожидающие внешних событий (ввода-вывода, таймеров, сетевых ответов), не блокируют исполнение остальных частей программы. Вместо ожидания поток «делегирует» продолжение вычисления диспетчеру событий, а результат возвращается через колбэки/фьючерсы/корутины.
Для подготовки к ЕГЭ по информатике тема полезна тем, что:
Синхронность, конкурентность, параллелизм
Синхронное выполнение: вызов f() занимает поток до возврата результата.
Асинхронное выполнение: вызов f_async() инициирует операцию и немедленно возвращает управляющий объект (колбэк/фьючерс/задача). Продолжение выполняется позже.
Конкурентность – логическое переплетение независимых операций; параллелизм – фактическая одновременность на разных ядрах. Асинхронность даёт конкурентность без обязательного параллелизма.
Событийная система и цикл событий
Рассмотрим абстракцию:
E = <Q, H, S, δ, π>
Q – очередь событий,
H – набор обработчиков,
S – внутреннее состояние,
δ – функция переходов состояния,
π – планировщик (event loop).
Цикл событий (event loop) извлекает событие из Q, находит обработчик из H, применяет переход δ, возможно планирует новые события.
Единицы работы
Callback – функция-продолжение.
Future/Promise – контейнер результата, меняющий состояние {pending, fulfilled, rejected}.
Coroutine (async/await) – кооперативная единица, при await отдаёт управление планировщику, сохраняя стек (машина состояний).
Модель колбэков
Граф продолжений – ориентированный ациклический граф (DAG), где вершины – обработчики, рёбра – зависимость «после завершения A запустить B».
Правило К1: каждое ребро должно соответствовать единственному источнику завершения (иначе многократные вызовы приведут к дублированию побочных эффектов).
Фьючерсы/промисы
Определим интерфейс:
Future<T> = <state, value, error, continuations>
add_continuation(f): если state = pending, добавить f; иначе выполнить немедленно.
Правило F1 (монотонность): состояние переходит pending → fulfilled или pending → rejected ровно один раз.
Правило F2 (идемпотентность): продолжение должно корректно обрабатываться при немедленном и отложенном запуске (отсутствие гонок и повторной подписки).
Корутины (async/await)
Корутина – это конечный автомат:
M = <Q, q0, F, δ>
q0 – начальное состояние (до первого await),
δ(q, событие_результата) → q', пока не достигнем финала F.
Правило C1 (точки приостановки): выносите побочные эффекты после await, либо обеспечьте идемпотентность – при повторном запуске из сохранённого состояния не нарушайте инварианты.
Отношение happens-before
Определим →_hb как наименьшее отношение, содержащее:
Программный порядок внутри задачи,
Синхронизацию (постановка колбэка/результата и его выполнение),
Транзитивность.
Если A →_hb B, то эффекты A видимы в B.
Правило М1: обмен данными между асинхронными частями возможен только через точки синхронизации (завершение фьючерса, отправка в канал, мьютекс, атомики).
Атомарность и неизменяемость
Неизменяемые структуры безопаснее для передачи между задачами.
Атомарные операции гарантируют корректный подсчёт/флаги без мьютексов в простых случаях.
Правило М2: общие изменяемые объекты избегайте; если неизбежно – инкапсулируйте доступ в одну «петлю» (actor-подход).
Готовность vs завершение
Модель готовности (select/poll/epoll/kqueue): планировщик уведомляет, что дескриптор готов к чтению/записи.
Модель завершения (completion ports): планировщик доставляет событие о факте завершения операции.
Оценка трудоёмкости (идеализировано)
Пусть N – число активных дескрипторов, E – событий за интервал. Тогда:
Время обработки ≈ O(E) + O(cost_dispatch) + O(cost_callbacks)
Пробуждение цикла ≈ O(log N) или O(1) (зависит от ядра и структуры очереди)
Правило I/O1: объединяйте мелкие операции в пачки (batch) и используйте конвейеризацию.
Таймауты
Для операции с бюджетом Δt вводим ограничение:
если t_факт > Δt, операция отменяется и публикуется ошибка Timeout.
Правило T1: у каждой внешней операции должен быть таймаут по умолчанию.
Отмена
Передаётся токен CancelToken.
Правило T2: отмена кооперативна: обработчик регулярно проверяет токен в точках ожидания; отмена означает «больше неинтересен результат», а не «жёстко прервать» исполнение в произвольный момент.
Правило S1: не вызывайте блокирующие
операции внутри цикла событий. Оборачивайте в пул рабочих (thread-pool) с обратной доставкой результата.
Правило S2: при переходе «синхронный → асинхронный» верните задачу/фьючерс; при переходе «асинхронный → синхронный» используйте ожидание вне
главного цикла, чтобы избежать взаимоблокировки.
Правило P1: в конвейере длина очереди ограничена:
0 ≤ size(queue) ≤ Qmax
Если size(queue) = Qmax, производитель должен ждать или отбрасывать (в соответствии с политикой).
Пусть:
Тогда среднее время ответа (грубо):
Tresp ≈ t_cpu + (1 - p_overlap) * t_io + Toverhead
Где Toverhead – накладные расходы диспетчера и синхронизаций.
Пропускная способность:
Throughput ≈ 1 / Tresp (для одного «рабочего слота»).
С увеличением конкурентности C (числа одновременно выполняемых задач) при I/O-доминировании достигается
Throughput ≈ C / Tresp
до насыщения диска/сети/ЦП.

R1 Подстановки/кавычки: сохраняйте границы аргументов (в асинхронных CLI-задачах).
F1 Future меняет состояние ровно один раз.
C1 Побочные эффекты – после await или делайте их идемпотентными.
M1 Данные делите через синхронизацию; immutable – предпочтительны.
I/O1 Батчируйте операции; избегайте мелких чтений/записей.
T1 Каждая внешняя операция имеет таймаут.
T2 Отмена кооперативна и наблюдаема в точках ожидания.
S1 Не блокируйте event loop; CPU-участки → в пул.
P1 Ограничивайте буферы; реализуйте backpressure.
QA Покрывайте крайние случаи: таймаут, отмена, частичный успех.
Связь с ЕГЭ по информатике
Упражнение 1. Автомат корутины и идемпотентность
Задача. Смоделируйте корутину FetchThenStore, выполняющую: (1) сетевой запрос, (2) запись результата в хранилище. Сетевая часть может завершиться с ошибкой/таймаутом; запись – повторяемая операция.
a) Постройте конечный автомат M = <Q, q0, F, δ> с состояниями: Start, AwaitNet, NetOk, NetErr, Store, Done.
b) Сформулируйте инвариант: «если запись начата, то в хранилище окажется ровно одна версия результата».
c) Предложите стратегию: делать запись после await и делать её идемпотентной
(например, PUT по ключу с той же версией). Докажите, что повторный запуск из Store не нарушает инвариант.
Упражнение 2. Backpressure и предел производительности
Дано. Источник генерирует элементы со скоростью λ_prod = 5000 элементов/с. Обработчик справляется со скоростью λ_cons = 3000 элементов/с. Буфер ограничен Qmax = 1000.
a) Выведите условие устойчивости конвейера: λ_prod ≤ λ_cons.
b) Через сколько секунд переполнится буфер, если не вводить ограничение источника?
Δλ = λ_prod - λ_cons = 2000 эл/с
t_overflow = Qmax / Δλ = 1000 / 2000 = 0.5 с
c) Предложите политику: «блокировать/замедлять» источник при size(queue) ≥ Qmax·0.8 и сбрасывать «наименее важные» элементы при size(queue)=Qmax.
Упражнение 3. Анализ happens-before
Сценарий. Задача A заполняет структуру S и завершает фьючерс F значением ссылки на S. Задача B, дожидаясь F, читает S.
a) Опишите →_hb: запись в S в A должна предшествовать чтению S в B через факт завершения F.
b) Приведите контрпример гонки: чтение B без ожидания F.
c) Сформулируйте правило: «все публикации данных – до fulfill, все чтения – после await».
Упражнение 4. Смешивание блокирующих вызовов
Ситуация. Внутри обработчика события выполняется синхронное чтение файла размером 100 МБ.
a) Объясните, почему это «замораживает» цикл событий.
b) Предложите переписывание: вынести операцию в пул рабочих и вернуть фьючерс.
c) Оцените Tresp до/после:
До: Tresp ≈ t_io(100МБ) (блокирует всех)
После: Tresp ≈ Toverhead + min(t_io, перекрыто остальными задачами)
Упражнение 5. Таймауты, отмена и ретраи
Условие. Вызов внешнего API имеет среднее время 250 мс, дисперсию высокую; SLA – не более 700 мс на запрос.
a) Выберите таймаут запроса Δt (например, 500 мс) и количество ретраев k (например, 1) с джиттером 50–150 мс.
b) Докажите, что P(превышение SLA) снижается при разумном k, но излишний k увеличивает нагрузку; сформулируйте критерий остановки: «отмена цепочки при срабатывании внешнего таймера 700 мс».
c) Запишите псевдоконтракт:
{ P: Δt ≤ 500мс, k ≤ 1, общий таймер 700мс }
call_async(req) -> Future<res|Timeout|Cancel>
{ Q: либо res до 700мс, либо контролируемая ошибка Timeout/Cancel }
Асинхронное программирование – это строгая вычислительная модель с чёткими правилами синхронизации, публикации данных и управления временем. В ней центральны: граф продолжений, конечные автоматы корутин, happens-before, таймаут/отмена, backpressure и конвейеры. Осваивая эти принципы и решая приведённые упражнения, вы формируете навыки, критичные для задач ЕГЭ: формализацию состояний, доказательство корректности, оценку асимптотик и аккуратную работу с пограничными случаями.