Асинхронность, промисы и циклы событий — Примеры
Пример 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 ничего больше не выполняется.