Proper maintenance and care of multi-threading locks

次の戦略は、コードがデッドロックフリーであることを保証するために使用されます(一般的に4番目のコフマン条件:循環待機に対処することによって)。

  1. コードを構造化して、同時に取得する必要があるロックが1つだけになるようにします。
  2. 常に、以下の表に示された順序で共有ロックを取得してください。
  3. 無制限の再帰を必要とする構造は避けてください。

Locks

以下は、システムに存在するすべてのロックと、それらを使用するためのメカニズムであり、デッドロックの可能性を回避します(ここではオーストリッチアルゴリズムは許可されていません):

以下は確実にリーフロック(レベル1)であり、他のロックを取得しようとするべきではありません:

  • セーフポイント

    このロックは、JL_LOCKJL_UNLOCK によって暗黙的に取得されることに注意してください。レベル 1 のロックの場合は、_NOGC バリアントを使用してそれを回避してください。

    このロックを保持している間、コードはどのような割り当ても行わず、セーフポイントに到達してはいけません。割り当てを行う際、GCの有効化/無効化、例外フレームの入出力、ロックの取得/解放を行う際にはセーフポイントが存在することに注意してください。

  • shared_map

  • ファイナライザー

  • pagealloc

  • gcpermlock

  • flisp

  • jlinstackwalk (Win32)

  • ResourcePool<?>::mutex

  • RLST_mutex

  • llvm印刷ミューテックス

  • jlロックされたストリーム::ミューテックス

  • debuginfo_asyncsafe

  • 推論タイミングミューテックス

  • ExecutionEngine::SessionLock

    flisp自体はすでにスレッドセーフであり、このロックはjl_ast_context_list_tプールを保護するだけです。同様に、ResourcePool<?>::mutexesは関連するリソースプールを保護するだけです。

以下はリーフロック(レベル2)であり、内部でレベル1ロック(セーフポイント)をのみ取得します:

  • グローバルルーツロック
  • モジュール->ロック
  • JLDebuginfoPlugin::PluginMutex
  • newlyinferredmutex

以下はレベル3のロックであり、内部的にレベル1またはレベル2のロックのみを取得できます:

  • メソッド->writelock
  • タイプキャッシュ

以下はレベル4のロックであり、レベル1、2、または3のロックを取得するためにのみ再帰できます:

  • MethodTable->writelock

このポイントより上でロックを保持している間は、Juliaコードを呼び出すことはできません。

orc::ThreadSafeContext (TSCtx) ロックは、ロック階層の特別な位置を占めています。これらは LLVM のグローバルなスレッドセーフでない状態を保護するために使用されますが、任意の数のロックが存在する可能性があります。デフォルトでは、これらのロックは階層の他の部分と比較する目的でレベル 5 のロックとして扱われることがあります。TSCtx を取得するのは JIT の TSCtx プールからのみ行うべきであり、その TSCtx に対するすべてのロックはプールに戻す前に解放されるべきです。複数の TSCtx ロックを同時に取得する必要がある場合(再帰的コンパイルのため)、ロックはプールから借りた TSCtx の順序で取得するべきです。

以下はレベル5のロックです

  • JuliaOJIT::EmissionMutex

以下はレベル6のロックであり、下位レベルのロックを取得するためにのみ再帰することができます:

  • コード生成
  • jlモジュールミューテックス

以下はほぼルートロック(レベルエンド-1)であり、取得しようとする際にルートロックのみが保持されることを意味します:

  • タイプインフ

    これはおそらく最も厄介なものでしょう。なぜなら、型推論は多くのポイントから呼び出される可能性があるからです。

    現在、ロックはコード生成ロックとマージされています。なぜなら、互いに再帰的に呼び出し合うからです。

次のロックはIO操作を同期します。他のロックを保持している間にI/O(警告メッセージやデバッグ情報の印刷など)を行うと、厄介で見つけにくいデッドロックが発生する可能性があるため、十分に注意してください!

  • iolock

  • 個別のThreadSynchronizersロック

    これは、iolockを解除した後も保持される可能性がありますが、保持している間にiolockを取得しようとしないように非常に注意してください。

  • Libdl.LazyLibrary ロック

以下はルートロックであり、これを取得しようとする際には他のロックを保持してはならないことを意味します:

  • トップレベル

    これは新しいタイプを作成したり、新しいメソッドを定義したりするなどのトップレベルのアクションを試みている間に保持されるべきです:ステージされた関数内でこのロックを取得しようとすると、デッドロック状態を引き起こします!

    さらに、任意の トップレベルの式と安全に並行して実行できるコードがあるかどうかは不明であるため、すべてのスレッドが最初にセーフポイントに到達する必要があるかもしれません。

Broken Locks

以下のロックは壊れています:

  • トップレベル

    現在は存在しません。

    修正: 作成する

  • モジュール->ロック

    これはデッドロックに対して脆弱です。なぜなら、順番に取得されていることを確信できないからです。一部の操作(例えば import_module)にはロックが欠けています。

    修正: jl_modules_mutex に置き換えますか?

  • loading.jl: requireregister_root_module

    このファイルには潜在的に多数の問題があります。

    修正: ロックが必要です

Shared Global Data Structures

これらのデータ構造は、共有される可変グローバル状態であるため、各々ロックが必要です。これは上記のロック優先リストの逆リストです。このリストには、その単純さのためにレベル1のリーフリソースは含まれていません。

MethodTableの変更 (def, cache) : MethodTable->writelock

型宣言 : トップレベルロック

タイプアプリケーション : タイプキャッシュロック

グローバル変数テーブル : モジュール->ロック

モジュールシリアライザー : トップレベルロック

JIT と型推論 : コード生成ロック

MethodInstance/CodeInstanceの更新 : Method->writelock, codegen lock

  • これらは建設時に設定され、不変です:

    • specTypes
    • sparam_vals
    • def
    • オーナー
  • これらは jl_type_infer によって設定されます(コード生成ロックを保持している間):

    • キャッシュ
    • rettype
    • 推測された
    * valid ages
  • inInference フラグ:

    • jl_type_inferがすでに実行中のときに再発を迅速に回避するための最適化
    • 実際の状態(inferredを設定し、その後fptrを設定する)は、コード生成ロックによって保護されています。
  • 関数ポインタ:

    • これらの遷移は、NULL から値に一度だけ行われ、コード生成ロックが保持されている間に行われます。
  • コード生成器キャッシュ(functionObjectsDeclsの内容):

    • これらは複数回遷移できますが、コード生成ロックが保持されている間のみです。
    • 古いバージョンを使用することは有効であり、新しいバージョンのためにブロックすることもできますので、レースは無害です。ただし、コードがメソッドインスタンス内の他のデータ(例えば rettype)を参照せず、かつコード生成ロックを保持していない限り、それが調整されていると仮定することに注意してください。

LLVMContext : コード生成ロック

メソッド : メソッド->writelock

  • ルーツ配列(シリアライザーとコード生成)
  • 呼び出し / 専門分野 / tfunc 修正