#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`実行前に部分的に初期化されたモジュールが公開される可能性があります

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

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