Асинхронность, промисы и циклы событий — Примеры

Пример 1 — Один блокирующий вызов, заморозивший все запросы

Асинхронный сервис обслуживает три одновременных запроса, и каждый обработчик пишет время начала и конца. Средний обработчик выполняет синхронное чтение файла длительностью 500 мс — перед вами наблюдаемый журнал; скажите, почему два запроса, не имеющих ничего общего, задерживаются, и где место исправлению.

12:00:00.000  A start
12:00:00.001  B start
12:00:00.002  C start
12:00:00.502  A end        <- A вообще не читает файл
12:00:00.503  B end
12:00:00.504  C end

B вызывает: value = read_file_sync(path)   # 500 мс ожидания

Решение

цикл однопоточный, поэтому read_file_sync занимает единственный поток,
и никакой другой обратный вызов не выполняется эти 500 мс. A и C были
готовы сразу и всё равно ждали.

исправить: используйте асинхронное чтение, уступающее циклу:
     value = await read_file(path)

Ожидание ввода-вывода — не работа, поэтому асинхронное чтение регистрирует операцию, возвращает управление циклу и будит продолжение, когда данные приходят; остальные обратные вызовы тем временем выполняются. Синхронное чтение занимает единственный поток, имеющийся у цикла, и каждая задача в работе выстраивается за ним. Ловушка — в форме симптома: медленным становится не один эндпоинт, а все сразу. Читается это как проблема ёмкости, а не как один блокирующий вызов.

Пример 2 — Проверка, сработавшая после того, как значение уже изменилось

Обработчик читает статус заказа и ожидает вызов оплаты. Затем он отказывается продолжать, если статус больше не «в ожидании». Скажите, почему проверка ничего не защищает, и перепишите её так, чтобы она что-то значила.

before = order.status          # "pending"
await charge(order)            # во время этого await работают другие задачи
if order.status != "pending":
    raise Conflict()           # задуманная проверка
apply(order)

Решение

await приостанавливает этот обработчик и возвращает управление циклу,
поэтому другие обратные вызовы — в том числе тот, что может изменить
order.status, — выполняются до проверки. Сравнение значения, прочитанного
до приостановки, со значением, прочитанным после, описывает уже
случившееся чередование; предотвратить его оно не может.

исправить: перечитайте и действуйте одним шагом, без приостановки между ними:
await charge(order)
if not order.mark_applied_if_pending():   # один атомарный шаг
    raise Conflict()
apply(order)

await приостанавливает этот обработчик и возвращает управление циклу, поэтому другие обратные вызовы выполняются до проверки — в том числе тот, что может изменить order.status. Сравнение значения, прочитанного до приостановки, со значением, прочитанным после, описывает уже случившееся чередование; предотвратить его оно не может. Копирование значения до приостановки и сравнение с ним после даёт проверку, зависящую от того самого чередования, которое она должна была исключить. Ловушка в том, что обычно всё выглядит работающим, потому что при низкой конкурентности во время await ничего больше не выполняется.