「ユニバーサルコンストラクション」は、線形化を可能にするシーケンシャルオブジェクトのラッパークラスです(並行オブジェクトの強い整合性条件)。たとえば、Javaでの[1]からの適応された待機なしの構成は次のとおりです。これは、インターフェースWFQ
を満たす待機なしのキューの存在を前提としています(スレッド間のコンセンサスが1回だけ必要です)。 Sequential
インターフェースを想定しています:
public interface WFQ<T> // "FIFO" iteration
{
int enqueue(T t); // returns the sequence number of t
Iterable<T> iterateUntil(int max); // iterates until sequence max
}
public interface Sequential
{
// Apply an invocation (method + arguments)
// and get a response (return value + state)
Response apply(Invocation i);
}
public interface Factory<T> { T generate(); } // generate new default object
public interface Universal extends Sequential {}
public class SlowUniversal implements Universal
{
Factory<? extends Sequential> generator;
WFQ<Invocation> wfq = new WFQ<Invocation>();
Universal(Factory<? extends Sequential> g) { generator = g; }
public Response apply(Invocation i)
{
int max = wfq.enqueue(i);
Sequential s = generator.generate();
for(Invocation invoc : wfq.iterateUntil(max))
s.apply(invoc);
return s.apply(i);
}
}
この実装は非常に遅いため、満足のいくものではありません(すべての呼び出しを覚えており、適用するたびにそれを再生する必要があります-履歴サイズに線形ランタイムがあります)。 WFQ
およびSequential
インターフェイスを(合理的な方法で)拡張して、新しい呼び出しを適用するときにいくつかの手順を保存できる方法はありますか?
待機なしのプロパティを失うことなく、これをより効率的にすることができますか(履歴サイズの線形ランタイムではなく、できればメモリ使用量も減ります)。
説明
「ユニバーサル構造」は、スレッド安全ではないがスレッド互換のオブジェクトを受け入れる[1]によって構成されたと確信している用語であり、Sequential
インターフェースによって一般化されています。待機フリーキューを使用して、最初の構築は、待機フリーでもあるオブジェクトのスレッドセーフなlinearizableバージョンを提供します(これは決定論と停止を前提としていますapply
操作)。
この方法は、各ローカルスレッドをクリーンな状態から開始し、記録されたevery操作を適用するため、効率的ではありません。いずれにせよ、これはWFQ
を使用してすべての操作を適用する順序を決定することで効率的に同期を達成するため機能します。apply
を呼び出すすべてのスレッドは同じローカルSequential
オブジェクト。同じシーケンスのInvocation
sが適用されます。
私の質問は、「開始状態」を更新するバックグラウンドクリーンアッププロセスを(たとえば)導入して、最初から再起動する必要がないかどうかです。これは、開始ポインターを持つアトミックポインターを持つほど単純ではありません。これらの種類のアプローチは、待機なしの保証を簡単に失います。私の疑いは、他のいくつかのキューベースのアプローチがここで機能するかもしれないということです。
専門用語:
apply
は、そのスレッドに対して実行された、おそらく制限された数の命令で終了します。apply
操作が他のスレッドで実行されている場合にのみ、無制限の実行時間の可能性を認めます。通常、オプティミスティック同期スキームはこのカテゴリに分類されます。リクエストに応じて動作する例(現在は期限切れにならないページ)
[1] HerlihyおよびShavit、The Art of Multiprocessor Programming。
これを実現する方法の説明と例を次に示します。不明な部分がある場合はお知らせください。
スレッドインデックスは、アトミックに増分された方法で適用されます。これは、AtomicInteger
という名前のnextIndex
を使用して管理されます。これらのインデックスは、ThreadLocal
から次のインデックスを取得してそれをインクリメントすることで初期化するnextIndex
インスタンスを介してスレッドに割り当てられます。これは、各スレッドのインデックスが初めて取得されるときに発生します。 ThreadLocal
は、このスレッドが作成した最後のシーケンスを追跡するために作成されます。 0に初期化されています。順次ファクトリオブジェクト参照が渡され、保存されます。サイズがAtomicReferenceArray
の2つのn
インスタンスが作成されます。テールオブジェクトは各参照に割り当てられ、Sequential
ファクトリによって提供される初期状態で初期化されています。 n
は、許可されるスレッドの最大数です。これらの配列の各要素は、対応するスレッドインデックスに「属しています」。
これは面白い仕事をする方法です。次のことを行います。
次に、シーケンスループが始まります。現在の呼び出しがシーケンスされるまで続行されます。
decideNext()
によって返されたシーケンスに現在のスレッドのヘッド参照を設定します上記のネストされたループの鍵は、decideNext()
メソッドです。それを理解するには、Nodeクラスを調べる必要があります。
このクラスは、二重リンクリストのノードを指定します。このクラスではあまりアクションはありません。ほとんどのメソッドは、かなり自明であるはずの単純な取得メソッドです。
これは、シーケンスが0の特別なノードインスタンスを返します。呼び出しによって置き換えられるまで、これは単にプレースホルダーとして機能します。
seq
:シーケンス番号、-1に初期化(シーケンスなしを意味)invocation
:apply()
の呼び出しの値。建設時に設定します。next
:フォワードリンクの場合はAtomicReference
。割り当てられると、これは決して変更されませんprevious
:シーケンス時に割り当てられ、truncate()
によってクリアされた逆方向リンクのAtomicReference
このメソッドはNodeの1つにすぎず、重要なロジックがあります。簡単に言えば、ノードはリンクリストの次のノードの候補として提供されます。 compareAndSet()
メソッドは、参照がnullかどうかをチェックし、nullの場合は、参照を候補に設定します。参照がすでに設定されている場合は、何もしません。この操作はアトミックなので、2つの候補が同時に提供された場合、1つだけが選択されます。これにより、1つのノードのみが次のノードとして選択されることが保証されます。候補ノードが選択されると、そのシーケンスが次の値に設定され、前のリンクがこのノードに設定されます。
ノードまたはannounce
配列のノードを使用して、最後にシーケンスされたノード(チェックされている場合)でdecideNext()
を呼び出した場合、2つの可能性があります。1.ノードが正常にシーケンスされた2.いくつか他のスレッドがこのスレッドを横取りしました。
次のステップは、この呼び出し用にノードが作成されたかどうかを確認することです。これは、このスレッドが正常にシーケンスしたか、他のスレッドがannounce
配列から取得してシーケンスしたために発生する可能性があります。シーケンスされていない場合は、このプロセスが繰り返されます。それ以外の場合、呼び出しは、このスレッドのインデックスでのアナウンス配列をクリアし、呼び出しの結果値を返すことで終了します。アナウンス配列は、ノードのガベージコレクションの妨げとなるノードへの参照が残っていないことを保証するために消去され、リンクリスト内のすべてのノードがその時点からヒープ上に存続します。
呼び出しのノードが正常にシーケンスされたので、呼び出しを評価する必要があります。これを行うには、最初のステップは、この呼び出しの前の呼び出しが評価されていることを確認することです。彼らがいない場合、このスレッドは待機しませんが、すぐに機能します。
ensurePrior()
メソッドは、リンクリスト内の前のノードをチェックすることにより、この作業を行います。状態が設定されていない場合、前のノードが評価されます。 Nodeこれは再帰的です。前のノードの前のノードが評価されていない場合は、そのノードに対してevaluateを呼び出し、以下同様に続きます。
前のノードに状態があることがわかったので、このノードを評価できます。最後のノードが取得され、ローカル変数に割り当てられます。この参照がnullの場合、他のスレッドがこの参照を横取りし、このノードをすでに評価していることを意味します。状態を設定します。それ以外の場合、前のノードの状態は、このノードの呼び出しとともにSequential
オブジェクトのapplyメソッドに渡されます。返された状態がノードに設定され、truncate()
メソッドが呼び出され、不要になったノードからの逆方向リンクがクリアされます。
前方に移動するメソッドは、すべてのヘッド参照がこの先のノードをまだ指していなければ、このノードへの移動を試みます。これは、スレッドが呼び出しを停止した場合に、そのヘッドが、不要になったノードへの参照を保持しないようにするためです。 compareAndSet()
メソッドは、他のスレッドがノードを取得してから変更していない場合にのみ、ノードを更新するようにします。
このアプローチを単純にロックフリーではなく待機フリーにするための鍵は、スレッドスケジューラが各スレッドに必要なときに優先順位を与えるとは想定できないことです。各スレッドがそれ自体のノードのシーケンスを単純に試みた場合、スレッドが負荷の下で継続的に横取りされる可能性があります。この可能性を説明するために、各スレッドは最初に、シーケンスを取得できない他のスレッドを「支援」しようとします。
基本的な考え方は、各スレッドがノードを正常に作成すると、割り当てられるシーケンスは単調に増加するということです。 1つまたは複数のスレッドが継続的に別のスレッドを横取りしている場合、announce
配列内のシーケンスされていないノードを検索するためのインデックスが使用されます。特定のノードをシーケンスしようとしているすべてのスレッドが別のスレッドによって継続的に横取りされている場合でも、最終的にはすべてのスレッドがそのノードをシーケンスしようとします。説明のために、3つのスレッドを持つ例を作成します。
開始点では、3つのスレッドすべてのhead要素とannounce要素がtail
ノードに向けられています。各スレッドのlastSequence
は0です。
この時点で、スレッド1が呼び出されて実行されます。アナウンス配列の最後のシーケンス(ゼロ)をチェックします。これは、現在インデックスを作成するようにスケジュールされているノードです。ノードをシーケンスし、lastSequence
が1に設定されます。
スレッド2が呼び出されて実行されるようになりました。最後のシーケンス(ゼロ)でアナウンス配列をチェックし、ヘルプが不要であることを確認して、呼び出しのシーケンスを試みます。成功し、lastSequence
が2に設定されました。
スレッドが実行され、announce[0]
のノードがすでにシーケンスされており、それ自体の呼び出しをシーケンスしていることも確認できます。 lastSequence
が3に設定されました。
これでThread 1が再び呼び出されます。インデックス1のアナウンス配列をチェックし、すでに配列されていることがわかります。同時に、スレッド2が呼び出されます。インデックス2のアナウンス配列をチェックし、すでに配列されていることがわかります。 Thread 1とThread 2の両方が、独自のノードのシーケンスを試行します。 スレッド2勝ち、それが呼び出しのシーケンスになります。 lastSequence
が4に設定されています。その間、スレッド3が呼び出されました。インデックスitlastSequence
(mod 3)をチェックし、announce[0]
のノードがシーケンスされていないことを確認します。 スレッド2は、2回目の試行時にスレッド1と同時に呼び出されます。 スレッド1スレッド2によって作成されたばかりのノードであるannounce[1]
でシーケンスされていない呼び出しを検出します。 Thread 2'sの呼び出しをシーケンスして、成功します。 スレッド2は、それがannounce[1]
で自分のノードであることを検出し、シーケンスされました。次に、lastSequence
を5に設定します。次にThreadが呼び出され、announce[0]
に配置されたスレッド1のノードがまだシーケンスされていないことを検出し、そうしようとします。一方、スレッド2も呼び出され、スレッド3をプリエンプトします。これにより、ノードがシーケンスされ、lastSequence
が6に設定されます。
悪いスレッド1。 スレッドがシーケンスを作成しようとしているにもかかわらず、両方のスレッドがスケジューラによって継続的に妨害されています。しかし、現時点では。 Thread 2もannounce[0]
(6 mod 3)を指すようになりました。 3つのスレッドはすべて、同じ呼び出しのシーケンスを試みるように設定されています。どのスレッドが成功しても、次にシーケンスされるノードは、スレッド1の待機中の呼び出しになります。つまり、announce[0]
によって参照されるノードです。
これは避けられません。スレッドがプリエンプトされるためには、他のスレッドがノードをシーケンスしている必要があり、そうすることで、スレッドは継続的にlastSequence
を先に進めます。特定のスレッドのノードが継続的にシーケンスされない場合、最終的にすべてのスレッドは、アナウンス配列のそのインデックスを指します。支援しようとしているノードがシーケンス処理されるまで、スレッドは何もしません。最悪のシナリオは、すべてのスレッドが同じシーケンス処理されていないノードを指している場合です。したがって、呼び出しをシーケンス処理するために必要な時間は、スレッドの数の関数であり、入力のサイズではありません。
私の以前の答えは実際には質問に適切に答えていませんが、OPはそれを有用であると見なしているので、そのままにしておきます。質問のリンクにあるコードに基づいて、これが私の試みです。私はこれについて本当に基本的なテストのみを行いましたが、平均を適切に計算しているようです。これが適切に待機なしであるかどうかに関するフィードバックを歓迎しました。
[〜#〜] note [〜#〜]:ユニバーサルインターフェイスを削除してクラスにしました。 UniversalをSequentialsで構成することも、Sequentialsで構成することも、不必要な複雑さのように思えますが、何かが足りない可能性があります。平均的なクラスでは、状態変数をvolatile
としてマークしました。コードを機能させるためにこれは必要ありません。保守的になり(スレッド化の良いアイデア)、各スレッドがすべての計算を(1回だけ)実行しないようにします。
public interface Sequential<E, S, R>
{
R apply(S priorState);
S state();
default boolean isApplied()
{
return state() != null;
}
}
public interface Factory<E, S, R>
{
S initial();
Sequential<E, S, R> generate(E input);
}
import Java.util.concurrent.ConcurrentLinkedQueue;
public class Universal<I, S, R>
{
private final Factory<I, S, R> generator;
private final ConcurrentLinkedQueue<Sequential<I, S, R>> wfq = new ConcurrentLinkedQueue<>();
private final ThreadLocal<Sequential<I, S, R>> last = new ThreadLocal<>();
public Universal(Factory<I, S, R> g)
{
generator = g;
}
public R apply(I invocation)
{
Sequential<I, S, R> newSequential = generator.generate(invocation);
wfq.add(newSequential);
Sequential<I, S, R> last = null;
S prior = generator.initial();
for (Sequential<I, S, R> i : wfq) {
if (!i.isApplied() || newSequential == i) {
R r = i.apply(prior);
if (i == newSequential) {
wfq.remove(last.get());
last.set(newSequential);
return r;
}
}
prior = i.state();
}
throw new IllegalStateException("Houston, we have a problem");
}
}
public class Average implements Sequential<Integer, Average.State, Double>
{
private final Integer invocation;
private volatile State state;
private Average(Integer invocation)
{
this.invocation = invocation;
}
@Override
public Double apply(State prior)
{
System.out.println(Thread.currentThread() + " " + invocation + " prior " + prior);
state = prior.add(invocation);
return ((double) state.sum)/ state.count;
}
@Override
public State state()
{
return state;
}
public static class AverageFactory implements Factory<Integer, State, Double>
{
@Override
public State initial()
{
return new State(0, 0);
}
@Override
public Average generate(Integer i)
{
return new Average(i);
}
}
public static class State
{
private final int sum;
private final int count;
private State(int sum, int count)
{
this.sum = sum;
this.count = count;
}
State add(int value)
{
return new State(sum + value, count + 1);
}
@Override
public String toString()
{
return sum + " / " + count;
}
}
}
private static final int THREADS = 10;
private static final int SIZE = 50;
public static void main(String... args)
{
Average.AverageFactory factory = new Average.AverageFactory();
Universal<Integer, Average.State, Double> universal = new Universal<>(factory);
for (int i = 0; i < THREADS; i++)
{
new Thread(new Test(i * SIZE, universal)).start();
}
}
static class Test implements Runnable
{
final int start;
final Universal<Integer, Average.State, Double> universal;
Test(int start, Universal<Integer, Average.State, Double> universal)
{
this.start = start;
this.universal = universal;
}
@Override
public void run()
{
for (int i = start; i < start + SIZE; i++)
{
System.out.println(Thread.currentThread() + " " + i);
System.out.println(System.nanoTime() + " " + Thread.currentThread() + " " + i + " result " + universal.apply(i));
}
}
}
ここに投稿するときに、コードをいくつか編集しました。大丈夫ですが、問題がある場合はお知らせください。