Ответ
Да, состояние гонки между корутинами возможно даже в одном потоке. Несмотря на то, что выполнение кооперативное (корутины не вытесняются произвольно), оно переключается в точках await
, и это может привести к недетерминированному поведению.
Почему это происходит:
Проблема возникает, когда операция "чтение-модификация-запись" разделена точкой await
. Одна корутина может прочитать значение, затем планировщик передает управление другой корутине, которая также читает старое значение, и в результате одно из обновлений теряется.
Классический пример с счетчиком:
import asyncio
counter = 0
async def increment():
global counter
# 1. Чтение
local_counter = counter
# 2. Точка переключения контекста. Другая корутина может выполниться здесь.
await asyncio.sleep(0)
# 3. Запись
counter = local_counter + 1
async def main():
tasks = [increment() for _ in range(1000)]
await asyncio.gather(*tasks)
# Ожидаемый результат: 1000
# Реальный результат: может быть любым числом < 1000
print(f"Итоговый счетчик: {counter}")
asyncio.run(main())
Решение:
Для защиты критических секций кода необходимо использовать примитивы синхронизации, такие как asyncio.Lock
.
# ... (код выше)
lock = asyncio.Lock()
async def increment_safe():
global counter
async with lock:
local_counter = counter
await asyncio.sleep(0)
counter = local_counter + 1
# ... (в main вызывать increment_safe)