Pythonの非同期実行におけるコンテキスト変数の伝播:contextvars.copy_context()徹底解説


コンテキスト変数 とは、コード実行中に保持される、名前付きの値のペアです。これらは、リクエスト ID、ユーザー ID、トレース情報などの情報を格納するために使用できます。

非同期フレームワーク とは、非同期プログラミングをサポートするライブラリまたはフレームワークを指します。asyncio は Python で最も人気のある非同期フレームワークの 1 つです。

マルチスレッド とは、複数のスレッドを使用してプログラムを実行することです。これにより、CPU リソースをより効率的に活用し、パフォーマンスを向上させることができます。

contextvars.copy_context() の仕組み

contextvars.copy_context() 関数は、現在のコンテキスト変数のスナップショットを作成し、新しいコンテキストオブジェクトとして返します。この新しいコンテキストオブジェクトは、別のコルーチンやスレッドに渡すことができます。

新しいコンテキストオブジェクトで実行されるコードは、元のコンテキスト変数の値にアクセスできます。ただし、新しいコンテキストオブジェクトで変更されたコンテキスト変数の値は、元のコンテキストには反映されません。

次の例は、contextvars.copy_context() 関数を使用して、コンテキスト変数を別のコルーチンに伝播する方法を示しています。

import asyncio
import contextvars

async def main():
    # コンテキスト変数を設定する
    context = contextvars.Context()
    context.enter_context(contextvars.ContextVar('user_id', 12345))

    # 非同期関数を実行する
    await asyncio.create_task(my_async_function())

async def my_async_function():
    # コンテキスト変数の値を取得する
    user_id = contextvars.copy_context().get('user_id')
    print(f"User ID: {user_id}")

if __name__ == "__main__":
    asyncio.run(main())

この例では、main() 関数は user_id というコンテキスト変数を設定します。その後、my_async_function() という非同期関数を呼び出します。

my_async_function() 関数は contextvars.copy_context() 関数を使用して、現在のコンテキスト変数のスナップショットを取得します。その後、user_id コンテキスト変数の値を取得して印刷します。

contextvars.copy_context() 関数は、次のようなさまざまなユースケースで使用できます。

  • テストでのコンテキストのモックアップ
    テストにおいて、特定のコンテキスト値を設定して、コードの動作を検証するために使用できます。
  • マルチスレッド環境でのコンテキスト情報の共有
    ログやトレース情報などの情報を、複数のスレッド間で共有するために使用できます。
  • 非同期フレームワークでのコンテキスト情報の伝播
    リクエスト ID やユーザー ID などの情報を、非同期関数のチェーン全体に伝播するために使用できます。


import asyncio
import contextvars

async def main():
    # リクエスト ID を設定する
    request_id = contextvars.ContextVar('request_id')
    contextvars.enter_context(request_id.set(12345))

    # 非同期関数を実行する
    await my_async_function()

async def my_async_function():
    # コンテキスト変数の値を取得する
    request_id = contextvars.copy_context().get('request_id')
    print(f"Request ID: {request_id}")

    # 別の非同期関数を実行する
    await another_async_function()

async def another_async_function():
    # コンテキスト変数の値を取得する
    request_id = contextvars.copy_context().get('request_id')
    print(f"Request ID: {request_id}")

if __name__ == "__main__":
    asyncio.run(main())

my_async_function() 関数は contextvars.copy_context() 関数を使用して、現在のコンテキスト変数のスナップショットを取得します。その後、request_id コンテキスト変数の値を取得して印刷し、another_async_function() という別の非同期関数を呼び出します。

another_async_function() 関数は contextvars.copy_context() 関数を使用して、現在のコンテキスト変数のスナップショットを取得します。その後、request_id コンテキスト変数の値を取得して印刷します。

このコードを実行すると、次の出力がコンソールに表示されます。

Request ID: 12345
Request ID: 12345

この例では、contextvars.copy_context() 関数を使用して、ログメッセージにトレース ID を追加する方法を示します。

import contextvars
import threading
import time

def worker(trace_id):
    # コンテキスト変数の値を取得する
    trace_id = contextvars.copy_context().get('trace_id')

    # ログメッセージにトレース ID を追加する
    print(f"[Trace ID: {trace_id}] Starting work")

    # シミュレートされた作業を実行する
    time.sleep(1)

    print(f"[Trace ID: {trace_id}] Work completed")

if __name__ == "__main__":
    # トレース ID を設定する
    trace_id = contextvars.ContextVar('trace_id')
    contextvars.enter_context(trace_id.set(42))

    # スレッドを起動する
    thread1 = threading.Thread(target=worker)
    thread2 = threading.Thread(target=worker)

    thread1.start()
    thread2.start()

    thread1.join()
    thread2.join()

この例では、__main__() 関数は trace_id というコンテキスト変数を設定します。その後、worker() という関数を呼び出す 2 つのスレッドを起動します。

worker() 関数は contextvars.copy_context() 関数を使用して、現在のコンテキスト変数のスナップショットを取得します。その後、trace_id コンテキスト変数の値を取得し、ログメッセージに含めます。

[Trace ID: 42] Starting work
[Trace ID: 42] Work completed
[Trace ID: 42] Starting work
[Trace ID: 42] Work completed


以下に、いくつかの代替方法と、それぞれの長所と短所を説明します。

スレッドローカルストレージ

スレッドローカルストレージは、各スレッドに固有の値を格納するために使用できる Python の組み込み機能です。contextvars.copy_context() の代替として使用できますが、いくつかの制限があります。

長所

  • 標準ライブラリに組み込まれているため、追加のライブラリをインストールする必要がない
  • シンプルで理解しやすい

短所

  • 複数のスレッド間で同じコンテキスト変数を共有することはできない
  • 非同期コードでは使用できない

カスタムコンテキストマネージャー

カスタムコンテキストマネージャーを使用して、コンテキスト変数の値を伝播することができます。これは、より複雑な方法ですが、より柔軟な制御を提供します。

長所

  • コードをより構造化および再利用しやすくする
  • 複数のスレッド間で同じコンテキスト変数を共有できる
  • 非同期コードと同期コードの両方で使用できる

短所

  • コードが複雑になる可能性がある
  • contextvars.copy_context() よりも記述量が多くなる

第三者ライブラリ

contextvars モジュールの機能を拡張するいくつかのサードパーティライブラリがあります。これらのライブラリは、追加の機能やユーティリティを提供することができます。

長所

  • コードをより簡潔に記述できる場合がある
  • contextvars モジュールの機能を拡張できる

短所

  • ライブラリの使用方法を習得する必要がある
  • 追加のライブラリをインストールする必要がある

使用する代替方法は、特定のニーズによって異なります。

  • contextvars モジュールの機能を拡張する必要がある場合は、サードパーティライブラリ が良い選択肢です。
  • コードをより構造化および再利用しやすくしたい場合は、カスタムコンテキストマネージャー が良い選択肢です。
  • 非同期コードや複数のスレッド間でコンテキスト変数を共有する必要がある場合は、カスタムコンテキストマネージャー または サードパーティライブラリ が必要になります。
  • シンプルで使いやすいソリューションが必要な場合は、スレッドローカルストレージ が良い選択肢です。

contextvars.copy_context() は、Python の Concurrent Execution におけるコンテキスト変数の管理に役立つ強力なツールです。しかし、状況によっては、この関数の代替方法を使用する方が適切な場合があります。