これがどのように達成されるかの説明と例があります。明確でない部分があるかどうかを教えてください。
ソースの要点
ユニバーサル
初期化:
スレッドインデックスは、アトミックインクリメント方式で適用されます。これは、AtomicInteger
という名前を使用して管理されnextIndex
ます。これらのインデックスはThreadLocal
、次のインデックスを取得してnextIndex
インクリメントすることで自身を初期化するインスタンスを介してスレッドに割り当てられます。これは、各スレッドのインデックスが最初に取得されるときに初めて発生します。A ThreadLocal
は、このスレッドが作成した最後のシーケンスを追跡するために作成されます。0に初期化されます。順次ファクトリオブジェクト参照が渡され、保存されます。AtomicReferenceArray
サイズの2つのインスタンスが作成されますn
。テールオブジェクトは各参照に割り当てられ、Sequential
ファクトリーによって提供される初期状態で初期化されています。 n
許可されるスレッドの最大数です。これらの配列の各要素は、対応するスレッドインデックスに「属します」。
適用方法:
これは興味深い仕事をする方法です。次のことを行います。
- この呼び出しの新しいノードを作成します:mine
- 現在のスレッドのインデックスで、この新しいノードをアナウンス配列に設定します
その後、シーケンスループが開始されます。現在の呼び出しがシーケンス化されるまで続行されます。
- このスレッドによって作成された最後のノードのシーケンスを使用して、アナウンス配列内のノードを見つけます。これについては後で詳しく説明します。
- ステップ2でノードが見つかった場合、まだシーケンス化されていません。続行します。そうでない場合は、現在の呼び出しに焦点を合わせます。これは、呼び出しごとに他の1つのノードのみを支援しようとします。
- ステップ3で選択されたノードが何であれ、最後にシーケンスされたノードの後にシーケンスを試行し続けます(他のスレッドが干渉する可能性があります)。成功に関係なく、
decideNext()
上記のネストされたループの鍵はdecideNext()
メソッドです。それを理解するには、Nodeクラスを調べる必要があります。
ノードクラス
このクラスは、二重リンクリストのノードを指定します。このクラスではそれほど多くのアクションはありません。ほとんどのメソッドは、簡単に取得できるメソッドであり、一目瞭然です。
テール方式
これにより、シーケンス0の特別なノードインスタンスが返されます。これは、呼び出しによって置き換えられるまで、単にプレースホルダーとして機能します。
プロパティと初期化
seq
:-1に初期化されたシーケンス番号(シーケンスなしを意味する)
invocation
:の呼び出しの値apply()
。建設時に設定します。
next
:AtomicReference
前方リンク用。一度割り当てられると、これは変更されません
previous
:AtomicReference
シーケンス時に割り当てられ、クリアされた逆方向リンク用truncate()
次を決める
このメソッドは、非自明なロジックを持つNodeで1つだけです。一言で言えば、ノードはリンクリストの次のノードになる候補として提供されます。compareAndSet()
この方法は、それの参照がnullであるかどうかを確認し、もしそうであれば、候補への参照を設定します。参照が既に設定されている場合、何もしません。この操作はアトミックであるため、2つの候補が同時に提供された場合、1つだけが選択されます。これにより、1つのノードのみが次のノードとして選択されることが保証されます。候補ノードが選択されると、そのシーケンスは次の値に設定され、前のリンクはこのノードに設定されます。
Universalクラスのapplyメソッドに戻る...
decideNext()
ノードまたは配列のノードのいずれかを使用して、最後にシーケンスされたノード(チェックされた場合)を呼び出した場合announce
、2つの可能性があります。
次のステップでは、この呼び出し用にノードが作成されたかどうかを確認します。これは、このスレッドがそれを正常にシーケンスしたか、他のスレッドがそれannounce
を配列からピックアップしてシーケンスしたために発生する可能性があります。シーケンスされていない場合、プロセスが繰り返されます。それ以外の場合、このスレッドのインデックスでアナウンス配列をクリアし、呼び出しの結果値を返すことにより、呼び出しは終了します。アナウンス配列は、ノードがガベージコレクションされないようにするために残されたノードへの参照がないことを保証するためにクリアされます。
評価方法
呼び出しのノードが正常にシーケンスされたので、呼び出しを評価する必要があります。それを行うための最初のステップは、この呼び出しに先行する呼び出しが評価されたことを確認することです。彼らが持っていない場合、このスレッドは待機しませんが、すぐにその作業を行います。
EnsurePriorメソッド
このensurePrior()
メソッドは、リンクリスト内の前のノードをチェックすることでこれを行います。状態が設定されていない場合、前のノードが評価されます。これが再帰的なノード。前のノードよりも前のノードが評価されていない場合、そのノードの評価などを呼び出します。
前のノードに状態があることがわかったので、このノードを評価できます。最後のノードが取得され、ローカル変数に割り当てられます。この参照がnullの場合、他のスレッドがこの参照を横取りし、すでにこのノードを評価していることを意味します。状態を設定します。それ以外の場合、前のノードの状態はSequential
、このノードの呼び出しとともにオブジェクトのapplyメソッドに渡されます。返された状態がノードに設定され、truncate()
メソッドが呼び出され、不要になったノードからの後方リンクをクリアします。
MoveForwardメソッド
前方への移動方法は、それらがまださらに何かを指していなければ、すべてのヘッド参照をこのノードに移動しようとします。これは、スレッドが呼び出しを停止した場合、そのヘッドが不要になったノードへの参照を保持しないようにするためです。compareAndSet()
この方法は、それが検索されたので、他のスレッドがそれを変更されていない場合は必ず我々は唯一のノードを更新するようになります。
配列と支援を発表
単にロックフリーではなく、このアプローチを待機フリーにするための鍵は、必要なときにスレッドスケジューラが各スレッドに優先順位を与えると想定できないことです。各スレッドがそれ自身のノードのシーケンスを単純に試みた場合、負荷がかかった状態でスレッドが継続的に横取りされる可能性があります。この可能性を説明するために、各スレッドはまず、シーケンス化できない他のスレッドを「助け」ようとします。
基本的な考え方は、各スレッドがノードを正常に作成すると、割り当てられるシーケンスは単調に増加するということです。1つまたは複数のスレッドが継続的に別のスレッドを横取りしている場合、announce
配列内のシーケンスされていないノードを見つけるために使用されるインデックスは前方に移動します。現在、特定のノードをシーケンスしようとしているすべてのスレッドが別のスレッドによって継続的にプリエンプトされる場合でも、最終的にはすべてのスレッドがそのノードをシーケンスしようとします。説明のために、3つのスレッドで例を構築します。
開始点では、3つのスレッドのすべてのhead要素とAnnounce要素がtail
ノードを指しています。lastSequence
各スレッドのは0です。
この時点で、スレッド1は呼び出しで実行されます。アナウンス配列の最後のシーケンス(ゼロ)を確認します。これは、現在インデックスを作成するようにスケジュールされているノードです。ノードをシーケンスし、lastSequence
1に設定されます。
スレッド2は呼び出しで実行されるようになりました。最後のシーケンス(ゼロ)でアナウンス配列をチェックし、ヘルプを必要としないため、呼び出しのシーケンスを試みます。成功し、現在lastSequence
は2に設定されています。
スレッド3が実行され、ノードannounce[0]
が既にシーケンス化されていることと、独自の呼び出しをシーケンス化することも確認されます。現在lastSequence
は3に設定されています。
これで、スレッド1が再び呼び出されます。インデックス1のアナウンス配列をチェックし、既に配列されていることを見つけます。同時に、スレッド2が呼び出されます。インデックス2のアナウンス配列をチェックし、既に配列されていることを検出します。スレッド1とスレッド2の両方が、独自のノードのシーケンスを試行するようになりました。 スレッド2が勝ち、その呼び出しをシーケンスします。それはだlastSequence
4一方に設定されている、スレッド3が呼び出されました。インデックスit lastSequence
(mod 3)をチェックし、ノードannounce[0]
がシーケンスされていないことを検出します。 スレッド2は、スレッド1が2回目の試行中に同時に呼び出されます。 スレッド1スレッド2announce[1]
によって作成されたばかりのノードである、シーケンスなしの呼び出しを見つけます。スレッド2の呼び出しのシーケンスを試み、成功します。 スレッド2は自身のノードを見つけ、シーケンスされています。5に設定されます。 次に、スレッド3が呼び出され、スレッド1が配置されたノードがまだシーケンスされていないことが検出され、シーケンスが試行されます。一方、スレッド2も呼び出され、スレッド3をプリエンプション処理します。ノードをシーケンスし、6に設定します。announce[1]
lastSequence
announce[0]
lastSequence
悪いスレッド1。にもかかわらず、スレッド3は、それをシーケンスしようとしている、両方のスレッドは継続的にスケジューラによって阻止されています。しかし、この時点で。スレッド2もannounce[0]
(6 mod 3)を指しています。3つのスレッドはすべて、同じ呼び出しをシーケンスするように設定されています。どのスレッドが成功しても、シーケンスされる次のノードは、スレッド1の待機中の呼び出し、つまりによって参照されるノードになりますannounce[0]
。
これは避けられません。スレッドがプリエンプトされるためには、他のスレッドがノードをシーケンス処理する必要があります。そうすることで、スレッドは常にlastSequence
先に進みます。特定のスレッドのノードが連続してシーケンスされていない場合、最終的にすべてのスレッドは、アナウンス配列のインデックスを指します。助けようとしているノードがシーケンス化されるまで、スレッドは他に何もしません。最悪のシナリオは、すべてのスレッドが同じシーケンス化されていないノードを指していることです。したがって、呼び出しの順序付けに必要な時間は、入力のサイズではなく、スレッドの数の関数です。