Deadlock, race conditions, async/await, thread safety
Асинхронный код сложнее отладить. Ошибки конкурентности проявляются редко и случайно. Code review — ваш главный инструмент поимки этих багов.
# ❌ Race condition: два запроса читают одно значение
counter = 0
async def increment():
global counter
current = counter # Чтение: counter = 0
await asyncio.sleep(0) # Переключение контекста!
counter = current + 1 # Запись: counter = 1
# Два параллельных вызова:
# Вызов 1: читает 0, записывает 1
# Вызов 2: читает 0 (пока вызов 1 спит!), записывает 1
# Ожидалось: 2, получилось: 1# ✅ С блокировкой
import asyncio
lock = asyncio.Lock()
counter = 0
async def increment():
global counter
async with lock: # Только один вызов одновременно
current = counter
await asyncio.sleep(0)
counter = current + 1
# ✅ Или через атомарные операции (если поддерживает библиотека)
from aioredis import Redis
redis = Redis()
await redis.incr('counter') # АтомарноЧто проверять:
# ❌ Deadlock: два замка в разном порядке
lock_a = asyncio.Lock()
lock_b = asyncio.Lock()
async def task1():
async with lock_a: # Захватил A
await asyncio.sleep(0.1)
async with lock_b: # Ждёт B
...
async def task2():
async with lock_b: # Захватил B
await asyncio.sleep(0.1)
async with lock_a: # Ждёт A — DEADLOCK!
...# ✅ Всегда захватывать в одинаковом порядке
async def task1():
async with lock_a:
async with lock_b:
...
async def task2():
async with lock_a: # Тот же порядок!
async with lock_b:
...
# ✅ Или один замок на всё
async def task1():
async with global_lock:
...# ❌ Забыт await: корутина не выполняется
async def fetch_users():
users = db.query_users() # Возвращает корутину, не выполняется!
return users
# ✅ Правильно
async def fetch_users():
users = await db.query_users()
return users
# ✅ Mypy поймает эту ошибку с --strict-async# ❌ Блокирующий I/O блокирует весь event loop
async def process_file(path):
with open(path) as f: # Блокирующий вызов!
data = f.read()
return data
# ✅ Асинхронный I/O
async def process_file(path):
async with aiofiles.open(path) as f:
data = await f.read()
# ✅ Или в executor для CPU-bound
import asyncio
async def process_file(path):
loop = asyncio.get_event_loop()
data = await loop.run_in_executor(None, read_file_sync, path)# ❌ Последовательные запросы
async def fetch_all(user_ids):
results = []
for user_id in user_ids:
result = await fetch_user(user_id) # Ждём каждый запрос
results.append(result)
return results
# ✅ Параллельные запросы
async def fetch_all(user_ids):
tasks = [fetch_user(user_id) for user_id in user_ids]
return await asyncio.gather(*tasks) # Все параллельно# ❌ Не thread-safe
cache = {}
def get_cached(key, factory):
if key not in cache:
cache[key] = factory() # Race condition!
return cache[key]
# ✅ Thread-safe
import threading
cache = {}
lock = threading.Lock()
def get_cached(key, factory):
with lock:
if key not in cache:
cache[key] = factory()
return cache[key]
# ✅ Или через lru_cache (thread-safe в Python 3.2+)
from functools import lru_cache
@lru_cache(maxsize=128)
def get_cached(key):
return expensive_computation(key)# ❌ Не thread-safe: общий список
class Worker:
def __init__(self, results=[]): # Один список на все экземпляры!
self.results = results
def add_result(self, result):
self.results.append(result) # Race condition!
# ✅ Thread-safe
class Worker:
def __init__(self, results=None):
self.results = results if results is not None else []
self._lock = threading.Lock()
def add_result(self, result):
with self._lock:
self.results.append(result)Ключевая мысль: Асинхронные баги проявляются случайно и редко. Проверяйте shared state, блокировки, забытые await и блокирующий код в async функциях.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.