Возможно ли состояние гонки (race condition) между корутинами в однопоточном asyncio

Ответ

Да, состояние гонки между корутинами возможно даже в одном потоке. Несмотря на то, что выполнение кооперативное (корутины не вытесняются произвольно), оно переключается в точках 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)