Ответ
contextvars — это модуль из стандартной библиотеки Python (с версии 3.7), который позволяет управлять контекстно-зависимым состоянием. Он решает проблему передачи данных по стеку вызовов без явной передачи их в качестве аргументов, что особенно важно в асинхронном коде.
Проблема с threading.local в asyncio
В asyncio несколько корутин (задач) могут выполняться в одном и том же потоке операционной системы. threading.local хранит данные, уникальные для потока, поэтому все корутины в этом потоке будут видеть одни и те же данные, что приводит к их смешиванию и некорректному поведению.
contextvars решает эту проблему, так как переменные, созданные с его помощью, привязаны к контексту выполнения (context), а не к потоку. Каждая задача asyncio имеет свой собственный контекст.
Как это работает
import asyncio
import contextvars
# Создаем контекстную переменную с именем и значением по умолчанию
request_id_var = contextvars.ContextVar('request_id', default='N/A')
async def process_request(req_id):
# Устанавливаем значение переменной для текущего контекста (задачи)
token = request_id_var.set(req_id)
print(f"[{request_id_var.get()}] Начало обработки...")
await asyncio.sleep(0.1)
await log_event()
# Восстанавливаем предыдущее значение переменной
request_id_var.reset(token)
async def log_event():
# Вложенная функция имеет доступ к значению из контекста
print(f"[{request_id_var.get()}] Произошло событие в логе.")
async def main():
await asyncio.gather(
process_request('req-1'),
process_request('req-2')
)
asyncio.run(main())
Вывод:
[req-1] Начало обработки...
[req-2] Начало обработки...
[req-1] Произошло событие в логе.
[req-2] Произошло событие в логе.
Как видно из примера, каждая задача process_request работает со своим значением request_id, даже если выполняется в одном потоке.
Основные сценарии использования:
- Трассировка запросов (request ID, correlation ID).
- Управление сессиями пользователей.
- Контекст транзакций базы данных в асинхронных ORM.
Ответ 18+ 🔞
А, слушай, вот эта штука — contextvars — это ж, блядь, такая магия из стандартной библиотеки Python (с версии 3.7, не меньше). Она нужна, чтобы таскать какие-то данные по всему стеку вызовов, не передавая их в каждую функцию, как дурак, аргументом. Особенно, когда у тебя асинхронщина, asyncio этот твой.
В чём, сука, проблема-то была?
Раньше-то, для потоков, был threading.local. Ну, вроде норм, да? Каждый поток — свои данные. Но тут приходит asyncio и говорит: «А я, блядь, в одном потоке могу десять корутин одновременно гонять!». И все эти корутины лезут в один threading.local и начинают там друг другу данные путать, как мудаки. Полный пиздец, одним словом.
А contextvars — он умный. Он привязывает переменные не к потоку, а к контексту выполнения. И каждая асинхронная задача получает свой собственный контекст, свою песочницу, блядь. Никто никому не мешает.
Как эта хуйня работает?
Смотри, вот тебе живой пример, чтобы не быть просто пиздаболом:
import asyncio
import contextvars
# Создаём контекстную переменную. Как паспорт, блядь. С дефолтным значением.
request_id_var = contextvars.ContextVar('request_id', default='N/A')
async def process_request(req_id):
# Устанавливаем значение для текущей задачи. Всё, теперь оно тут живёт.
token = request_id_var.set(req_id)
print(f"[{request_id_var.get()}] Начало обработки...")
await asyncio.sleep(0.1)
await log_event() # И вот тут, в глубине вызовов, оно доступно!
# Откатываем значение назад, чтоб не засорять. Чистота — залог здоровья.
request_id_var.reset(token)
async def log_event():
# Смотри-ка! Функция даже не знает про req_id, но достаёт его из контекста!
print(f"[{request_id_var.get()}] Произошло событие в логе.")
async def main():
# Запускаем две задачи одновременно. Они в одном потоке, но у каждой свой контекст!
await asyncio.gather(
process_request('req-1'),
process_request('req-2')
)
asyncio.run(main())
Что на выходе, ёпта?
[req-1] Начало обработки...
[req-2] Начало обработки...
[req-1] Произошло событие в логе.
[req-2] Произошло событие в логе.
Видишь? Каждая задача пронесла свой request_id через весь вызов, даже через log_event, которая нихрена про него не знала! Это ж, блядь, почти волшебство. Никакого глобального состояния, никакой путаницы.
Где это, сука, пригождается?
- Трассировка запросов. Чтобы в логах понимать, какая строчка к какому запросу относится.
request_id,correlation_id— всё это. - Сессии пользователей. Чтобы не таскать объект сессии через все функции.
- Транзакции в базах данных. Особенно в асинхронных ORM, где на один поток может быть куча соединений.
Короче, вещь. Полезная, блядь. Когда пишешь асинхронку — без неё как без рук, сразу начинается бардак и пиздец.