Ответ
Да, состояние гонки между корутинами возможно даже в одном потоке. Несмотря на то, что выполнение кооперативное (корутины не вытесняются произвольно), оно переключается в точках 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) Ответ 18+ 🔞
А, слушай, вот это классика, блядь! Состояние гонки в корутинах — это ж пиздец как просто устроено, а ловишь потом баги, как дурак. Все думают: "О, асинхронщина, один поток, вытеснения нет — красота!" А потом на тебе — хуй с горы, данные поехали.
В чём, сука, соль:
Проблема-то в том, что твоя операция "прочитал-поправил-записал" разваливается нахуй в точке 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, да?
# А на практике — любое число меньше, волнение ебать!
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)
Вот так, сука. Захватил лок, сделал свои делишки, отпустил. Все довольны, данные целы. А то без лока — чистая лотерея, в рот меня чих-пых.