42. インポートロック

マルチスレッド コードにおけるモジュールごとのインポート ロック、リエントラント インポート検出、およびデッドロック シナリオ。

#42. インポートロック

インポート ロックは、安全でない同時インポートを防止する同期機構です。 CPython では、インポートは名前の検索だけではありません。モジュールオブジェクトを作成したり、突然変異したりする可能性があります。sys.modules、任意の Python コードの実行、拡張モジュールの初期化、パッケージ属性の更新、ソース ファイルのコンパイル、バイトコード キャッシュの読み取り、パッケージ初期化コードの実行を行います。

ロックを行わないと、2 つのスレッドが同じモジュールを同時にインポートし、一貫性のないモジュール状態が観察される可能性があります。

インポートは実行であり、実行により共有ランタイム状態が変化するため、インポート ロックが存在します。

42.1 インポートにロックが必要な理由

このモジュールについて考えてみましょう。```python id="ez8v8a"

cache.py

print("initializing cache")

items = {}

def get(key): return items[key] ここで、これを同時に実行する 2 つのスレッドを考えてみましょう。python id="2pklaf" import cache 同期がないと、両方のスレッドが次のような可能性があります。text id="oy5qmp" create a module object insert or overwrite sys.modules["cache"] execute cache.py initialize items twice observe a partially initialized module bind different module objects


## 42.2 インポートによりグローバル ランタイム状態が変更される

インポートは共有状態にタッチします。

重要な共有構造には次のものがあります。```text id="yxk2bl"
sys.modules
sys.meta_path
sys.path
sys.path_hooks
sys.path_importer_cache
parent package attributes
module dictionaries
bytecode cache files
extension module state
```最も重要なことは、`sys.modules`。```python id="ul20gj"
import sys

print(type(sys.modules))

sys.modulesは通常の辞書ですが、モジュール ID の中心となります。同時インポートによって破損したモジュールの挿入または置換が行われた場合、ランタイムは重複したモジュールまたは不完全なモジュールを監視する可能性があります。

42.3 単純なメンタルモデル

ロックを使用した単純化されたインポートは次のようになります。```python id="d8ycx5" def import_module(name): lock = get_import_lock_for(name)

with lock:
    if name in sys.modules:
        return sys.modules[name]

    spec = find_spec(name)
    module = module_from_spec(spec)
    sys.modules[name] = module

    try:
        spec.loader.exec_module(module)
    except Exception:
        del sys.modules[name]
        raise

    return module

実際の実装にはさらに多くのケースがありますが、中心的な考え方は次のとおりです。text id="dsb489" for a given module name, only one thread should execute that module's initialization at a time


最新の CPython は、インポート機構でモジュール固有のインポート ロックを使用します。目標は、2 つのスレッドが同じモジュールを同時に初期化しないようにしながら、すべてのインポートの不必要なシリアル化を回避することです。

概念的には:```text id="r8nrgm"
import a        locks module name "a"
import b        locks module name "b"
import a again  waits for "a" if another thread is initializing it
```これにより、各モジュール ID の安全性を維持しながら、単一のグローバル インポート ロックよりも高い同時実行性が得られます。

ロック キーは完全修飾モジュール名です。```text id="ev5p7h"
json
json.decoder
email.message
package.submodule
```各名前には独自のインポート同期境界があります。

## 42.5 インポートロックとGILの比較

グローバル インタープリター ロックとインポート ロックは、さまざまな問題を解決します。

|メカニズム |目的 |
|---|---|
|ギル |インタプリタの実行と多くの内部オブジェクト操作を保護します。
|インポートロック |安全でない同時モジュールの初期化を防止します。

GIL によってインポート ロックの必要性がなくなるわけではありません。

モジュールのインポートでは、ファイル I/O の実行、任意の Python コードの実行、ネイティブ コードでの GIL の解放、または他の操作の待機を行うことができます。最初のインポートの進行中に、別のスレッドが同じインポートを試みることができます。

インポート ロックは、上位レベルの不変式を保護します。```text id="u3ib58"
one module name should not be initialized concurrently by multiple threads
```## 42.6 インポートは再入可能です

インポートにより、さらに多くのインポートがトリガーされる可能性があります。

例:```python id="4hqnm0"
# app.py
import config
import server
# server.py
import logging
import socket
```インポート中`app`輸入品`server`、インポート`server`他のモジュールをインポートします。

インポート ロック機構は、ネストされたインポートをサポートする必要があります。

概念的には:```text id="k2va9e"
import app
    lock app
    execute app.py
        import server
            lock server
            execute server.py
                import socket
                    lock socket
                    execute socket.py
```これは正常です。

1 つのモジュールをインポートしているスレッドは、最初のモジュールの初期化中に別のモジュールをインポートできる必要があります。

## 42.7 同じモジュールの再帰的インポート

同じモジュールの再帰的インポートは、循環インポートを通じて発生する可能性があります。```python id="z153co"
# a.py
import b
x = 1
# b.py
import a
y = 2
```いつ`a`輸入品`b`、 そして`b`輸入品`a`、の 2 番目のインポート`a`それ自体を待機してデッドロックしないようにする必要があります。

これが、インポート ロックで所有権と再帰を慎重に追跡する必要がある理由の 1 つです。

インポート システムは、部分的に初期化されたモジュールを返すことでこれを処理します。`sys.modules`適切な場合。

これにより、無限再帰と自己デッドロックは防止されますが、部分的に初期化された状態が公開されます。

## 42.8 部分的に初期化されたモジュール

インポート中に、CPython はモジュールを次の場所に配置します。`sys.modules`コードを実行する前に。

これは循環インポートをサポートします。

のために:```python id="4jdauu"
# a.py
import b
value = 1
```そして:```python id="ayh1bo"
# b.py
import a
print(a.value)
```モジュール`a`に存在します`sys.modules`前に`value`が割り当てられています。それで`b`輸入できる`a`、 しかし`a.value`まだ存在しないかもしれません。

インポート ロックにより、2 つの同時初期化が防止されます。不完全なモジュールが完全になるわけではありません。

さまざまな問題:

|問題 |インポートロックは役に立ちますか? |
|---|---|
|同じモジュールを初期化する 2 つのスレッド |はい |
|不完全なモジュールを監視する循環インポート |いいえ |
|モジュールのシャドウイング |いいえ |
|悪い`sys.path`注文 |いいえ |
|トップレベルの副作用 |同時重複を防止するだけ |

ロックはスレッドの安全性を提供します。依存関係構造は修正されません。

## 42.9 インポートロックの取得順序

インポートは依存関係チェーンを形成します。モジュールは、別のモジュールのインポート中に独自のインポート ロックを保持する場合があります。

例:```text id="m3czjg"
Thread 1:
    lock a
    import b
    wait for lock b

Thread 2:
    lock b
    import a
    wait for lock a
```これは古典的なデッドロック形状です。

CPython のインポート機構には、モジュール ロックに関するデッドロック検出が含まれています。循環待機を検出すると、永久にハングすることを回避し、代わりに部分的に初期化されたモジュール パスを処理できます。

重要な設計ポイントは、インポート ロックが再入可能および循環依存関係グラフを処理する必要があるということです。

## 42.10 スレッド化されたインポートの例

この例では、同じモジュールをインポートする複数のスレッドを開始します。```python id="2bqtk9"
# slowmod.py
import time

print("slowmod start")
time.sleep(2)
value = 42
print("slowmod end")
# main.py
import threading

def worker():
    import slowmod
    print(slowmod.value)

threads = [threading.Thread(target=worker) for _ in range(5)]

for t in threads:
    t.start()

for t in threads:
    t.join()
```予想される動作:```text id="mq93uj"
slowmod start
slowmod end
42
42
42
42
42
```モジュール本体は 1 回実行されます。他のスレッドはインポートが完了するまで待機し、キャッシュされたモジュールを使用します。

## 42.11 異なるモジュールの同時インポート

依存関係が許可されている場合、モジュールごとのロックを使用して、さまざまなモジュールを同時にインポートできます。

スレッド 1:```python id="jg44xg"
import alpha
```スレッド 2:```python id="vyvgaf"
import beta
```もし`alpha`そして`beta`は独立しているため、それらのインポートを同じモジュール ロックでブロックする必要はありません。

ただし、次の点については引き続き争う可能性があります。```text id="4h21u8"
the GIL
file-system operations
path importer caches
extension module initialization
shared package parents
custom finder state
```インポートの同時実行は可能ですが、インポートは依然として複雑なグローバル操作です。

## 42.12 親パッケージとサブモジュールのロック

対象:```python id="bbvi06"
import package.submodule
```インポートシステムをロードする必要があります`package`初め。

概念的には:```text id="9mmvrw"
lock package
initialize package
lock package.submodule
initialize package.submodule
bind package.submodule attribute
```複数のスレッドが同じパッケージの異なる子をインポートする場合:```python id="rp9s87"
import package.alpha
import package.beta
```親パッケージの初期化ステップを共有する場合があります。

親が初期化されると、子はパッケージ パスとファインダーの動作に従って、独自のモジュール ロックによって処理できるようになります。

## 42.13 グローバルインポートロックイン`_imp`CPython は、`_imp`モジュール。```python id="8j3c5t"
import _imp

print(_imp.lock_held())
```の`_imp`モジュールには次のような関数が含まれています。```text id="iqvyee"
acquire_lock
release_lock
lock_held
```これらは低レベルの実装インターフェイスです。通常の Python コードでは、アプリケーションの同期にこれらを使用しないでください。

これらは輸入機械と互換性のために存在します。

これらを誤って使用すると、プロセスがデッドロックしたり、インポート システムに干渉したりする可能性があります。

## 42.14 インポートロックとカスタムインポーター

カスタム ファインダーとローダーは、インポート同期下で呼び出される可能性があることを想定する必要があります。

ファインダは、可能であれば、低速、ブロック、またはリエントラントの動作を避ける必要があります。

ローダーの`exec_module`メソッドはモジュールコードを実行します。他のモジュールをインポートする場合があります。

ローダー形状の例:```python id="viqfze"
class Loader:
    def create_module(self, spec):
        return None

    def exec_module(self, module):
        module.value = 42
```もし`exec_module`他のモジュールをインポートすると、同じロック グラフに参加します。

カスタム インポーターは、一貫性のない順序で無関係なロックを取得しないようにする必要があります。そうしないと、CPython 独自のインポート ロック ロジックの外側でデッドロックが発生する可能性があります。

## 42.15 インポートロックと`sys.modules`ロックは、モジュールの初期化に関する重要なセクションを保護します。

重要な操作には次のようなものがあります。```text id="hu7f15"
checking sys.modules
creating the module
inserting the module
executing module code
removing module on failure
returning the initialized module
```最も敏感な瞬間は、実行前の挿入です。```text id="fzh60s"
sys.modules[name] = module
exec_module(module)
```この順序は循環インポートをサポートしますが、実行が終了する前に他のコードがモジュールを監視できることを意味します。

ロックにより、同じ名前をインポートする他のスレッドが再度実行されるのではなく、完了を待つことが保証されます。

## 42.16 インポートの失敗とロックの解除

インポートが失敗した場合は、ロックを解除する必要があります。

例:```python id="3sf7se"
# broken.py
raise RuntimeError("boom")
try:
    import broken
except RuntimeError:
    pass
```失敗した後、別のスレッドが永久にブロックされるべきではありません。

インポート システムは次のことを行う必要があります。```text id="rhkz4v"
release the module lock
clean up sys.modules when appropriate
propagate the exception
allow future import attempts
```後のインポートで再試行できます。```python id="36ur7b"
import broken
```失敗したモジュール オブジェクトが特別な機械によって意図的に残されない限り、モジュールは再度実行されます。

## 42.17 ネイティブ拡張モジュール

拡張モジュールはインポート ロックを複雑にします。

拡張モジュールは次のことを行うことができます。```text id="bc8bib"
run native initialization code
allocate process-global state
register types
import other modules
release the GIL
use static C variables
interact with subinterpreters
```インポート ロックにより、同じ拡張モジュール名の同時初期化が防止されますが、拡張機能の作成者は初期化コードを慎重に記述する必要があります。

最新のマルチフェーズ初期化は、拡張モジュールが C グローバルのみではなくモジュール オブジェクトごとに状態を保存するのに役立ちます。

## 42.18 サブインタープリター

サブインタープリターは別の次元を追加します。

各インタプリタは、多くのモジュールに対して独自のモジュール ディクショナリ状態を持っていますが、ネイティブ拡張モジュールは、別の設計がされていない限り、依然としてプロセス グローバル C 状態を持つ可能性があります。

インポート ロックは、インタープリターの状態と拡張機能の状態に関連して考慮する必要があります。

拡張機能の作成者にとって、これは次のことを意味します。```text id="l90pr8"
avoid mutable process-global module state
prefer per-module state
support multi-phase initialization
consider subinterpreter isolation
```インポート ロックは、インポートの同時競合を防ぎますが、拡張モジュールの状態がインタプリタ間で自動的に分離されるわけではありません。

## 42.19 インポートのロックとリロード`importlib.reload(module)`モジュールコードを再実行します。```python id="q3ox9f"
import importlib
import config

importlib.reload(config)
```リロードは既存のモジュール ディクショナリを変更するため、インポート状態と調整する必要があります。

リロード中、他のコードは以下への参照を保持している可能性があります。```text id="8a8ikh"
the module object
old functions
old classes
old constants
old imported names
```インポート ロックは、モジュール名のリロード/インポート操作の同時競合を防ぐことができますが、リロードは意味的に扱いにくいままです。

リロードでは、すべての外部参照が更新されるわけではありません。

## 42.20 インポートロックとモジュール状態の可視性

インポート ロックはインポート操作を制御します。モジュールグローバルをトランザクション化するわけではありません。

モジュール コードがインポート中にグローバル状態を変更する場合:```python id="uwbmge"
registry = []

registry.append("phase 1")
registry.append("phase 2")
ready = True
```循環インポートに関係するコードでは、次のことが観察される場合があります。```text id="spya66"
registry exists
registry contains only phase 1
ready does not exist yet
```インポート ロックは同時重複実行を防止しますが、部分的に初期化されたモジュールは循環インポートを通じて引き続き表示されます。

## 42.21 インポート時の競合の回避

アプリケーション コードは、インポート時の変更を最小限に抑える必要があります。

これを好みます:```python id="1kciks"
# service.py

class Service:
    ...

def create_service(config):
    return Service(config)
```これについて:```python id="5wtu0a"
# service.py

config = load_config()
service = Service(config)
service.start()
```2 番目のフォームはインポート時に作業を実行します。テストもリロードも難しくなり、インポート順序の影響を受けやすくなります。

インポート時の作業は通常、定義と安価な定数に限定する必要があります。

## 42.22 安全なトップレベルコード

安全なトップレベル モジュール コードには通常、次のものが含まれます。```text id="xbk1bu"
imports
constants
class definitions
function definitions
small table definitions
cheap feature detection
type aliases
```より危険なトップレベルのコードには次のものが含まれます。```text id="ippw6v"
network calls
database connections
thread startup
event loop startup
large file reads
global registration with side effects
process-wide configuration
monkey patching
```インポート ロックは初期化をシリアル化しますが、高価なインポートや副作用の多いインポートは依然として起動と同時実行に悪影響を及ぼします。

## 42.23 インポートロックとデッドインポート

「デッド インポート」とは、依存関係サイクルまたは外部ロックが原因でインポートを完了できないモジュールを待機するインポートです。

例:```python id="jt6y2f"
# a.py
import threading
import b

lock = threading.Lock()
# b.py
import a
```単純な循環インポートは通常、部分的なモジュールの可視性によって処理されます。ただし、インポート中にモジュール コードが外部ロックを取得したり、スレッドを開始したり、イベントを待機したりすると、デッドロックが発生しやすくなります。

インポート時にスレッドや外部ロックで待機しないようにします。

## 42.24 インポート中のカスタムロック

これは危険です:```python id="626g6o"
# registry.py
import threading

lock = threading.Lock()

with lock:
    import plugin
```もし`plugin`輸入品`registry`同じロックを取得しようとすると、プログラムがデッドロックする可能性があります。

より良いデザイン:```python id="9ez7pd"
# registry.py
import threading

lock = threading.Lock()
handlers = {}

def load_plugin(name):
    import importlib
    module = importlib.import_module(name)
    return module

def register(name, handler):
    with lock:
        handlers[name] = handler
```可能な限り、アプリケーションのロックをインポート依存関係サイクルの外側に保ちます。

## 42.25 ロックとプラグイン システムのインポート

プラグイン システムは多くの場合、モジュールを動的にインポートします。```python id="iixpre"
import importlib

def load_plugins(names):
    for name in names:
        importlib.import_module(name)
```インポート時にプラグインが自身を登録する場合、ロードはモジュール名ごとにシリアル化されますが、それでも共有レジストリは変更されます。

より安全なプラグイン設計により、インポートと登録が分離されます。```python id="d13tr6"
def load_plugin(name):
    module = importlib.import_module(name)
    return module.setup
```次に、制御されたフェーズで setup を呼び出します。```python id="p8oelv"
for setup in setups:
    setup(registry)
```これにより、初期化順序が明確になります。

## 42.26 インポートロックと起動パフォーマンス

インポート ロックは、スレッド プログラムの起動パフォーマンスに影響を与える可能性があります。

多くのスレッドが開始して依存関係を遅延インポートすると、いくつかのスレッドが同じインポートでブロックする可能性があります。

一般的な改善点は、シングルスレッド起動時に主要な依存関係をインポートすることです。```python id="e6dmwr"
def main():
    import logging
    import database
    import server

    server.run()
```これにより、ワーカー スレッドが開始される前に初期化がフロントロードされます。

サーバー プログラムの場合、通常のパターンは次のとおりです。```text id="iq8ig9"
configure process
import dependencies
initialize application
start worker threads or event loop
```ワーカー スレッドが大規模な依存関係グラフを個別に検出してインポートすることは避けてください。

## 42.27 インポート ロックの問題の診断

インポート ロックまたはインポート サイクルの問題の症状は次のとおりです。```text id="m5z0iy"
program hangs during import
thread dump shows imports in multiple threads
partially initialized module errors
module attributes missing only during startup
plugin loading deadlocks
reload behaves inconsistently
```便利なデバッグ ツール:```python id="1mccj7"
import sys

print(sys.modules.get("module_name"))
import _imp

print(_imp.lock_held())
```ハングの場合は、スレッド ダンプを使用します。```python id="csvfsw"
import faulthandler

faulthandler.dump_traceback()
```または、コマンドラインからフォールトハンドラーを有効にします。```bash id="7wjpm2"
python -X faulthandler app.py
```## 42.28 インポートのタイミングとロックの競合

インポートのタイミングは次の方法で検査できます。```bash id="gr9wta"
python -X importtime -c "import your_package"
```これは、インポート中に時間が費やされた場所をレポートします。

ロック競合を直接示すわけではありませんが、競合ポイントになる可能性のある遅いインポートを特定するのに役立ちます。

インポートの速度が遅い場合は、通常、注意が必要です。```text id="7c79oz"
inside worker startup
inside request paths
inside plugin discovery
inside command-line entry points
inside test collection
```## 42.29 ロックと非同期コードのインポート

非同期コードではインポートが非同期になりません。

内の import ステートメント`async def`コルーチンのその部分が実行されるときも、同期して実行されます。```python id="fg7lak"
async def handler():
    import heavy_module
    return heavy_module.run()
```最初の電話は`handler`インポートに到達すると、モジュールのロード中にイベント ループがブロックされる可能性があります。

非同期サーバーの場合は、イベント ループを開始する前に重い依存関係をインポートするか、高価な初期化を明示的な非同期起動フックに移動します。

## 42.30 インポートロックとマルチプロセス

各プロセスには、独自のインタープリター状態と独自のインポート状態があります。

マルチプロセッシングでは、子プロセスはモジュールを個別にインポートします。

これは、新しいインタプリタを生成するプラットフォームや開始メソッドではより重要です。

たとえば、スポーンスタイルのプロセス作成では、子プロセスがメインモジュールをインポートします。

このため、マルチプロセッシング コードには次のものが必要です。```python id="w0j45j"
if __name__ == "__main__":
    main()
```ガードがないと、子プロセスはインポート中にトップレベルのコードを再実行する可能性があります。

インポート ロックは、1 つのプロセス内のインポートのみを保護します。プロセス間でインポートを同期しません。

## 42.31 インポート ロックとバイトコード キャッシュの書き込み

CPython がソース モジュールをインポートするとき、読み取りまたは書き込みが行われる場合があります。`.pyc`ファイル。

そうしないと、同時インポートがバイトコード キャッシュの生成を中心に競合する可能性があります。

輸入機械では丁寧に対応させていただきます。それでも、バイトコード キャッシュは最適化であり、モジュール ID のソースではありません。

ID のソースは次のとおりです。```text id="ka9zk4"
module name in sys.modules
```バイトコード キャッシュには、後のインポートを高速化するためにコンパイルされたコード オブジェクトのみが保存されます。

## 42.32 インポートロックとファイルシステムの変更

インポート ロックはファイルシステムの状態を安定させません。

インポート中にファイルが作成、削除、または置換されると、動作が混乱する可能性があります。

例:```text id="50enmw"
deployment replacing package files during process startup
tests generating modules dynamically
plugin files being written while discovery runs
zip archive changed while importing
```安定した展開方法を使用してください。実行中のプロセスがインポート可能なコードをインポートしている間に、インポート可能なコードを変更しないようにします。

## 42.33 インポート ロックはアプリケーション ロックではありません

インポートを同期メカニズムとして使用しないでください。

これは良くないパターンです。```python id="xdy9zr"
def initialize_once():
    import initialize_side_effects
```インポート キャッシュに依存して初期化を 1 回実行します。

明示的な 1 回限りの初期化を優先します。```python id="q7vhsy"
_lock = threading.Lock()
_initialized = False

def initialize_once():
    global _initialized

    if _initialized:
        return

    with _lock:
        if _initialized:
            return
        do_initialization()
        _initialized = True
```インポート キャッシュはモジュール用です。アプリケーションのライフサイクルは明示的である必要があります。

## 42.34 設計ルール: インポートは退屈でなければなりません

最も安全な輸入は退屈です。

退屈なインポート:```text id="tjyc8p"
defines names
sets constants
imports dependencies
does no external work
finishes quickly
```驚くべき輸入品:```text id="8vcz4t"
starts threads
opens sockets
loads large models
registers global plugins
patches builtins
changes logging globally
reads mutable external config
```インポートロックにより、驚くべきインポートの際どさが軽減されます。それを簡単に理屈で説明することはできません。

## 42.35 最小インポートロックモデル

コンパクトなモデル:```text id="iwck6e"
Thread imports module M.
Import system acquires lock for M.
If M is already initialized, return it.
If M is initializing in another thread, wait.
If this thread is already involved in the cycle, use partial module state.
Create and cache M before execution.
Execute M.
On success, mark M initialized and release lock.
On failure, clean up and release lock.
```このモデルは、インポートが通常の使用に十分なスレッドセーフである理由、循環インポートが依然として不完全なモジュールを公開する可能性がある理由、およびインポート時の副作用が危険なままである理由を説明します。

## 42.36 重要なポイント

インポート ロックにより、同じモジュール名の同時初期化が防止されます。

ロックは、任意のモジュール セマンティクスではなく、モジュールのロードを保護します。

GIL とインポート ロックはさまざまな問題を解決します。

モジュールは実行中に他のモジュールをインポートできるため、インポートは再入可能です。

循環インポートはモジュールを挿入することで処理されます。`sys.modules`実行前に、部分的に初期化されたモジュールが公開される可能性があります。

カスタム インポーター、プラグイン、拡張モジュール、サブインタープリター、リロード、およびスレッド起動はすべて、インポート ロックの重要性を高めます。

アプリケーションを適切に設計すると、インポートが高速かつ決定的に行われ、外部の副作用がほとんどなくなります。