contextvars モジュールは、非同期処理でタスクごとに独立した変数を持つ仕組みを提供します。スレッドローカル変数の非同期版です。
問題の背景
グローバル変数や threading.local は、非同期タスク間で意図しない共有が起きます。
import asyncio
current_user = None
async def process_request(user):
global current_user
current_user = user
await asyncio.sleep(0.1)
print(f"Processing for {current_user}") # 別タスクの値になる可能性
async def main():
await asyncio.gather(
process_request("Alice"),
process_request("Bob"),
)
asyncio.run(main())
ContextVar で解決
import asyncio
from contextvars import ContextVar
current_user: ContextVar[str] = ContextVar("current_user")
async def process_request(user):
current_user.set(user)
await asyncio.sleep(0.1)
print(f"Processing for {current_user.get()}")
async def main():
await asyncio.gather(
process_request("Alice"),
process_request("Bob"),
)
asyncio.run(main())
# 正しく Alice と Bob が出力される
ContextVar は各タスクのコンテキストに値を保存するため、タスク間で干渉しません。
Token によるリセット
set() は Token を返し、これを使って以前の値に戻せます。
from contextvars import ContextVar
var: ContextVar[int] = ContextVar("var", default=0)
token = var.set(10)
print(var.get()) # 10
var.reset(token)
print(var.get()) # 0
実用例:リクエストID のトラッキング
from contextvars import ContextVar
import asyncio
import uuid
request_id: ContextVar[str] = ContextVar("request_id")
def log(message):
rid = request_id.get("unknown")
print(f"[{rid}] {message}")
async def handle_request():
request_id.set(str(uuid.uuid4())[:8])
log("Start processing")
await asyncio.sleep(0.1)
log("Done")
async def main():
await asyncio.gather(
handle_request(),
handle_request(),
)
asyncio.run(main())
contextvars はロギング、認証情報、データベース接続など、リクエストスコープの状態管理に最適です。