Практическая ситуация: в одном event loop первая корутина удерживает threading.Lock во время await, а вторая пытается захватить этот lock. Может ли event loop зависнуть?
Да, event loop может зависнуть. Вызов threading.Lock.acquire() блокирует поток синхронно; если этот поток является потоком event loop, он перестаёт обрабатывать другие корутины, включая первую, которая должна была продолжить работу и освободить lock.
Для взаимного исключения между корутинами следует использовать asyncio.Lock и не выполнять длительные операции внутри критической секции без необходимости.
threading.Lock создан для синхронизации потоков: если lock занят, поток обычно ждёт его освобождения блокирующим системным вызовом. Такой подход допустим для обычного многопоточного кода, где блокировка одного потока не останавливает остальные.
asyncio использует кооперативную модель: один event loop последовательно исполняет корутины в своём потоке. Поэтому синхронная блокировка этого потока нарушает основную модель asyncio — управление должно возвращаться event loop на точках await, а не блокироваться внутри обычного вызова.
Рассмотрим последовательность: первая корутина захватывает threading.Lock и до освобождения выполняет await. Event loop переключается на вторую корутину, а та вызывает acquire() для уже занятого lock.
Вторая корутина не приостанавливается кооперативно — она блокирует весь поток event loop. Первая корутина не может возобновиться после await, поэтому не может освободить lock. Возникает взаимная блокировка, хотя корутины работают в одном потоке и отдельных потоков в приложении может не быть.
Минимальная демонстрация проблемы:
После захвата lock функция first уступает управление на await. Затем second вызывает блокирующий acquire(). Event loop останавливается внутри этого вызова и больше не может возобновить first.
Для корутин нужен asyncio.Lock. Его ожидание является асинхронным: занятая корутина передаёт управление event loop, а ожидающие задачи не блокируют поток.
asyncio.Lock предназначен для задач, работающих в одном event loop, и не заменяет потоковый lock при доступе из разных потоков. Если общим ресурсом одновременно пользуются потоки, требуется отдельная потоковая синхронизация, но её блокирующие операции нельзя напрямую выполнять в event loop.
Даже с asyncio.Lock длительная работа внутри критической секции снижает параллелизм: остальные корутины будут ждать освобождения lock. Поэтому следует защищать только минимальный участок, а независимый ввод-вывод или вычисления выносить за пределы критической секции, если это сохраняет корректность.
В асинхронном сервисе несколько обработчиков обновляют локальный кэш. Один разработчик использовал threading.Lock, потому что тот уже применялся в синхронной версии компонента. Под нагрузкой запросы иногда переставали завершаться: один обработчик ждал ввод-вывод с захваченным lock, а другой блокировал event loop на попытке захвата того же lock.
Рассматривались варианты:
threading.Lock — корректно только при отсутствии блокирующего ожидания в event loop, что трудно гарантировать;asyncio.Lock — совместимо с моделью корутин, но защищает только доступ из соответствующего event loop;Выбрали asyncio.Lock, сократили критическую секцию до проверки и изменения записи, а сетевой запрос вынесли за пределы lock. В результате event loop перестал блокироваться, а сериализация конкурентных изменений сохранилась.
Нет. GIL регулирует выполнение Python-кода в потоках CPython, но не превращает блокирующий threading.Lock.acquire() в асинхронное ожидание. Event loop всё равно может остановиться внутри вызова, ожидающего освобождения пользовательского lock.
Кроме того, GIL и взаимное исключение решают разные задачи: GIL связан с исполнением Python-байткода, а lock защищает конкретный общий ресурс. Наличие GIL не делает произвольную последовательность операций безопасной и не защищает от зависания event loop.
Гарантии безопасности нет. Если lock свободен, вызов действительно быстро завершится, но при занятости он может заблокировать поток event loop на неопределённое время. Редкая вероятность блокировки не устраняет последствий: достаточно одного такого случая, чтобы остановить обработку всех задач в этом loop.
Использование допустимо только при строгой гарантии, что вызов никогда не будет ждать, либо если он выполняется вне event loop, например в отдельном потоке с корректной архитектурой обмена данными. Для обычной синхронизации корутин предпочтительнее asyncio.Lock.
asyncio.Lock предназначен для координации задач внутри асинхронной модели и не является универсальным межпоточным примитивом. Он не должен использоваться как средство защиты состояния, к которому одновременно обращаются разные потоки.
Если ресурс разделяется потоками, нужна потоковая синхронизация на границе потоков. Часто безопаснее изолировать ресурс в одном потоке или передавать операции в него через очередь, чтобы корутины не выполняли блокирующие методы напрямую.