ロックの管理
マルチスレッドのコードがデッドロックを含まないことを保証するために、Julia では次の戦略が使われています。これは基本的にコフマンの第四条件 (環状 wait) を避けるためのものです:
- ロックを一度に一つずつ取得するようにコードを構成する。
- 共有ロックは必ず同じ順番 (後述) で取得する。
- 制限のない再帰を必要とする構文を避ける。
ロック
Julia システムに存在するロックと発生する可能性のあるデッドロックを割けるための使用方法を次に示します1 (ダチョウのアルゴリズムは許されません)。
次に示すのは一番下のリーフロック (レベル 1 のロック) です。これらのロックを保持しているときに他の任意のロックを取得してはいけません:
-
safepoint_lock
このロックは
JL_LOCK
とJL_UNLOCK
によって自動的に取得されます。レベル 1 のロックに対してこの処理を避けるには_NOGC
がついたバージョンを使ってください。このロックを保持している間、コードはメモリのアロケートしてはならず、セーフポイントを配置してもいけません。メモリのアロケート・GC の有効化/無効化・例外フレームのエンター/リストア・ロックの取得/解放はどれもセーフポイントを配置します。
-
shared_map_lock
-
finalizers_lock
-
gc_perm_lock
-
flisp_lock
flisp 自身は既にスレッドセーフであり、このロックは
jl_ast_context_list_t
プールを保護します。 jl_in_stackwalk
(win32)
次に示すのはレベル 2 のリーフロックであり、内部でレベル 1 のロック (safepoint_lock
) だけを取得できます:
typecache_lock
Module->lock
次に示すのはレベル 3 のロックであり、内部でレベル 1 またはレベル 2 のロックだけを取得できます:
Method->writelock
次に示すのはレベル 4 のロックであり、内部でレベル 1・レベル 2・レベル 3 のロックだけを取得できます:
MethodTable->writelock
ここまでのロックを保持した状態で Julia コードを実行することは許されません。
次に示すのはレベル 6 のロックであり、内部ではレベルが 5 以下のロックだけを取得できます:
codegen_lock
jl_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
- 次の値は構築時に設定される不変値です:
specTypes
sparam_vals
def
- 次の値は
jl_type_infer
によって (codegen_lock
を保持した状態で) 設定されます:cache
rettype
inferred
- 正当な世界時 (
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
の更新