Какую проблему решает модуль contextvars в Python и в чем его преимущество перед threading.local

Ответ

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, где на один поток может быть куча соединений.

Короче, вещь. Полезная, блядь. Когда пишешь асинхронку — без неё как без рук, сразу начинается бардак и пиздец.