← Назад к вопросам
Возможно ли происхождение состояния гонки в asyncio?
3.0 Senior🔥 141 комментариев
#Асинхронность и многопоточность
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Состояние гонки в asyncio: миф и реальность
Частое заблуждение: "asyncio на одном потоке, поэтому race conditions невозможны." Это неправда. Race conditions в asyncio не только возможны — они часто встречаются и опасны.
1. Почему asyncio НЕ защищает от race conditions
Ключевой момент
Одноточность asyncio защищает от классических race conditions только если используется одна корутина за раз. Но asyncio часто запускает множество корутин параллельно!
import asyncio
shared_counter = 0
async def increment_counter():
global shared_counter
# Race condition возможна здесь!
temp = shared_counter # 1. Прочитали
await asyncio.sleep(0) # 2. Переключение контекста!
shared_counter = temp + 1 # 3. Записали старое значение
async def main():
# Запускаем 100 корутин параллельно
await asyncio.gather(
*[increment_counter() for _ in range(100)]
)
print(shared_counter) # Ожидаем: 100, получаем: ~50
asyncio.run(main())
# Race condition!
# shared_counter прочитан несколько раз перед записью
2. Где возникают race conditions
Проблема 1: Между await
import asyncio
shared_dict = {"value": 0}
async def unsafe_update():
# Прочитали
value = shared_dict["value"]
# ТОЧКА ПЕРЕКЛЮЧЕНИЯ! Другая корутина может выполниться
await asyncio.sleep(0)
# Записали (но значение уже устаревшее!)
shared_dict["value"] = value + 1
async def main():
await asyncio.gather(
unsafe_update(),
unsafe_update(),
unsafe_update()
)
print(shared_dict) # {"value": 1} вместо {"value": 3}
asyncio.run(main())
Проблема 2: Работа с БД
import asyncio
import asyncpg
async def transfer_money(from_id, to_id, amount):
# Race condition в банке!
# Шаг 1: Прочитали баланс
balance = await db.fetchval(
"SELECT balance FROM accounts WHERE id = $1",
from_id
)
# ПЕРЕКЛЮЧЕНИЕ! Другая корутина может тоже прочитать баланс
await asyncio.sleep(0)
# Шаг 2: Проверили хватает ли денег
if balance < amount:
return {"error": "Insufficient funds"}
# ПЕРЕКЛЮЧЕНИЕ! Пока мы проверяем, другая корутина может снять деньги
await asyncio.sleep(0)
# Шаг 3: Перевод денег
await db.execute(
"UPDATE accounts SET balance = balance - $1 WHERE id = $2",
amount,
from_id
)
# Результат: счёт может уйти в минус!
# Двух пользователей с 100 на счету одновременно переводят 100
# Оба видят баланс 100, оба переводят
# Итог: счёт с -100 вместо +0
3. Примеры реальных race conditions
Проблема 1: Двойное создание ресурса
import asyncio
connection = None
async def get_connection():
global connection
# Race condition!
if connection is None: # Две корутины одновременно проверяют
# Обе видят None
await asyncio.sleep(0.1) # Подключение
connection = "connected"
return connection
async def main():
# Запускаем две корутины одновременно
c1 = await get_connection()
c2 = await get_connection()
# Обе создали connection! Потеря ресурса!
asyncio.run(main())
Проблема 2: Инкремент счётчика
import asyncio
class Counter:
def __init__(self):
self.value = 0
async def increment(self):
# Race condition в коде выше
temp = self.value
await asyncio.sleep(0) # Переключение
self.value = temp + 1
async def main():
counter = Counter()
# 10 корутин инкрементируют
await asyncio.gather(
*[counter.increment() for _ in range(10)]
)
print(counter.value) # Ожидаем 10, получаем ~5
asyncio.run(main())
4. Как защитить код
Решение 1: Locks (Мьютексы)
import asyncio
class Counter:
def __init__(self):
self.value = 0
self.lock = asyncio.Lock()
async def increment(self):
# Получить lock
async with self.lock:
# Только одна корутина выполняет это одновременно
temp = self.value
await asyncio.sleep(0) # Переключение внутри lock
self.value = temp + 1
# lock освобождается
async def main():
counter = Counter()
await asyncio.gather(
*[counter.increment() for _ in range(10)]
)
print(counter.value) # 10 ✓
asyncio.run(main())
Решение 2: Semaphore (для лимита параллельности)
import asyncio
async def fetch_url(session, url, semaphore):
# Максимум 5 одновременных запросов
async with semaphore:
# Получить разрешение
async with session.get(url) as response:
return await response.text()
# Отпустить разрешение
async def main():
semaphore = asyncio.Semaphore(5)
urls = [f"http://example.com/{i}" for i in range(100)]
async with aiohttp.ClientSession() as session:
tasks = [fetch_url(session, url, semaphore) for url in urls]
results = await asyncio.gather(*tasks)
asyncio.run(main())
Решение 3: Queue (для координации)
import asyncio
async def producer(queue):
for i in range(10):
await queue.put(f"item-{i}")
await asyncio.sleep(0.1)
async def consumer(queue, worker_id):
while True:
try:
item = queue.get_nowait()
print(f"Worker {worker_id}: processing {item}")
await asyncio.sleep(0.5)
queue.task_done()
except asyncio.QueueEmpty:
break
async def main():
queue = asyncio.Queue()
# Продюсер добавляет в очередь
# Консьюмеры обрабатывают
# Race condition не возможна - очередь thread-safe!
await asyncio.gather(
producer(queue),
consumer(queue, 1),
consumer(queue, 2),
consumer(queue, 3)
)
asyncio.run(main())
Решение 4: Правильная работа с БД
import asyncio
import asyncpg
async def transfer_money_safe(from_id, to_id, amount):
# Использовать SELECT FOR UPDATE для lock
async with db.transaction():
# Читаем с блокировкой
balance = await db.fetchval(
"""
SELECT balance FROM accounts
WHERE id = $1
FOR UPDATE -- КЛЮЧ: блокируем строку!
""",
from_id
)
if balance < amount:
raise ValueError("Insufficient funds")
# Обновляем с гарантией
await db.execute(
"UPDATE accounts SET balance = balance - $1 WHERE id = $2",
amount,
from_id
)
# Безопасный transfer - race condition невозможна
5. Таблица: Когда нужны механизмы защиты
| Операция | Риск | Защита |
|---|---|---|
| Чтение + запись разных переменных | Низкий | Обычно ok |
| Чтение + запись одной переменной | Высокий | Lock |
| Несколько операций на одном ресурсе | Высокий | Lock или Transaction |
| Работа с очередью | Низкий | asyncio.Queue (встроено) |
| Работа с БД | Высокий | SELECT FOR UPDATE или Transact |
| Подсчёт счётчика | Высокий | Lock |
Резюме
ДА, race conditions в asyncio возможны и опасны!
Ошибка думать что одноточность = нет race conditions.
Основная причина: await это точка переключения контекста.
Защита:
- Locks для shared state
- Semaphores для лимита параллельности
- Queues для async-safe координации
- SELECT FOR UPDATE в БД
- Transactions для ACID
Правило: Если переменная или ресурс используется в нескольких корутинах одновременно — нужна защита!