ロックの管理
マルチスレッドのコードがデッドロックを含まないことを保証するために、Julia では次の戦略が使われています。これは基本的にコフマンの第四条件 (環状 wait) を避けるためのものです:
- ロックを一度に一つずつ取得するようにコードを構成する。
- 共有ロックは必ず同じ順番 (後述) で取得する。
- 制限のない再帰を必要とする構文を避ける。
ロック
Julia システムに存在するロックと発生する可能性のあるデッドロックを割けるための使用方法を次に示します1 (ダチョウのアルゴリズムは許されません)。
次に示すのは一番下のリーフロック (レベル 1 のロック) です。これらのロックを保持しているときに他の任意のロックを取得してはいけません:
-
safepoint_lockこのロックは
JL_LOCKとJL_UNLOCKによって自動的に取得されます。レベル 1 のロックに対してこの処理を避けるには_NOGCがついたバージョンを使ってください。このロックを保持している間、コードはメモリのアロケートしてはならず、セーフポイントを配置してもいけません。メモリのアロケート・GC の有効化/無効化・例外フレームのエンター/リストア・ロックの取得/解放はどれもセーフポイントを配置します。
-
shared_map_lock -
finalizers_lock -
gc_perm_lock -
flisp_lockflisp 自身は既にスレッドセーフであり、このロックは
jl_ast_context_list_tプールを保護します。 jl_in_stackwalk(win32)
次に示すのはレベル 2 のリーフロックであり、内部でレベル 1 のロック (safepoint_lock) だけを取得できます:
typecache_lockModule->lock
次に示すのはレベル 3 のロックであり、内部でレベル 1 またはレベル 2 のロックだけを取得できます:
Method->writelock
次に示すのはレベル 4 のロックであり、内部でレベル 1・レベル 2・レベル 3 のロックだけを取得できます:
MethodTable->writelock
ここまでのロックを保持した状態で Julia コードを実行することは許されません。
次に示すのはレベル 6 のロックであり、内部ではレベルが 5 以下のロックだけを取得できます:
codegen_lockjl_modules_mutex
次に示すのはほぼルートのロック (レベル end - 1 のロック) です。つまり、このロックの取得を試みるときに保持していてよいのはルートロックだけです:
-
typeinf_lock型推論は様々な場所で使われるので、おそらくこれは最も厄介なロックの一つです。
型推論とコード生成は再帰的にお互いを呼び出すので、現在このロックと
codegen_lockは同じものとして扱われています。
次のロックは IO 操作の同期に使われます。ここまでのロックを保持したまま任意の IO (例えば警告メッセージやデバッグ情報の出力) を行うと、見つけにくい致命的なデッドロックが引き起こされる可能性があります。気を付けて!
-
iolock -
個別の
ThreadSynchronizerロックこのロックは
iolockを解放した後に保持し続けても構わず、iolockを保持していなくても取得できます。ただしiolockを保持したままiolockの取得を試みないよう厳重な注意が必要です。
次に示すのがルートロックです。つまり、このロックの取得を試みるときに他のロックを保持していてはいけません:
-
toplevel_lockこのロックはトップレベルの操作 (新しい型の作成や新しいメソッドの定義) を試みる間に保持している必要があります: 被生成関数でこのロックを取得しようとするとデッドロックとなります!
また任意のトップレベル式を持つコードを安全に並列実行できるかどうかは事前に分かりません。そのため場合によっては最初に全てのスレッドをセーフポイントに到達させる必要があります。
壊れたロック
次のロックは壊れています:
-
toplevel_lockこのロックは現在存在しません。
修正: 作成する。
-
Module->lockこのロックが順序通りに取得されたかどうかが分からないので、デッドロックを起こしやすくなっています。また
import_moduleなど一部の操作はロックを使っていません。修正:
jl_modules_mutexで置き換える? -
loading.jl:requireとregister_root_moduleこのファイルには問題が多くある可能性があります。
修正: ロックが必要。
共有されるグローバルなデータ構造
次のデータ構造はグローバルに共有される可変な状態なので、それぞれ対応するロックが必要です。これは上記のロックの優先順序リストを反対にしたものです。レベル 1 のリーフリソースは簡単なのでここには示されていません。
MethodTableの改変 (def,cache,kwsorter):MethodTable->writelock- 型宣言:
toplevel_lock - 型注釈:
typecache_lock - グローバル変数のテーブル:
Module->lock - モジュールシリアライザ:
toplevel_lock - JIT/型推論:
codegen_lock -
MethodInstance/CodeInstanceの更新:Method->writelock,codegen_lock- 次の値は構築時に設定される不変値です:
specTypessparam_valsdef
- 次の値は
jl_type_inferによって (codegen_lockを保持した状態で) 設定されます:cacherettypeinferred- 正当な世界時 (
min_world/max_world)
-
inInferenceフラグ:jl_type_inferを実行しているときにjl_type_inferを再帰的に呼び出すのを素早く防止するための最適化です。- 実際の (
inferredとfptrを設定する処理の) 状態はcodegen_lockで保護されます。
-
関数ポインタ:
codegen_lockが保持された状態でNULLから実際の値へと一度だけ変更されます。
-
コード生成のキャッシュ (
functionObjectsDeclsの中身):- 複数回変更される場合がありますが、変更は必ず
codegen_lockが保持された状態で行われます。 - 古いバージョンのキャッシュを使う、あるいは新しいバージョンのキャッシュを待機してブロックするのは正当です。そのためコードがメソッドインスタンスに含まれる他のデータ (
rettypeなど) を参照しない限り、競合は良性です。
- 複数回変更される場合がありますが、変更は必ず
- 次の値は構築時に設定される不変値です:
-
LLVM コンテキスト:
codegen_lock - メソッド:
Method->writelock- ルート配列 (シリアライザとコード生成)
invoke/specializations/tfuncの更新