web-dev-qa-db-ja.com

ユニバーサル構造をより効率的にするにはどうすればよいですか?

「ユニバーサルコンストラクション」は、線形化を可能にするシーケンシャルオブジェクトのラッパークラスです(並行オブジェクトの強い整合性条件)。たとえば、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オブジェクト。同じシーケンスのInvocationsが適用されます。

私の質問は、「開始状態」を更新するバックグラウンドクリーンアッププロセスを(たとえば)導入して、最初から再起動する必要がないかどうかです。これは、開始ポインターを持つアトミックポインターを持つほど単純ではありません。これらの種類のアプローチは、待機なしの保証を簡単に失います。私の疑いは、他のいくつかのキューベースのアプローチがここで機能するかもしれないということです。

専門用語:

  1. 待機なし-スレッドの数やスケジューラーの意思決定に関係なく、applyは、そのスレッドに対して実行された、おそらく制限された数の命令で終了します。
  2. ロックフリー-上記と同じですが、無制限の数のapply操作が他のスレッドで実行されている場合にのみ、無制限の実行時間の可能性を認めます。通常、オプティミスティック同期スキームはこのカテゴリに分類されます。
  3. ブロッキング-スケジューラのなすがままの効率。

リクエストに応じて動作する例(現在は期限切れにならないページ)

[1] HerlihyおよびShavit、The Art of Multiprocessor Programming

16
VF1

これを実現する方法の説明と例を次に示します。不明な部分がある場合はお知らせください。

ソース付き要旨

ユニバーサル

初期化:

スレッドインデックスは、アトミックに増分された方法で適用されます。これは、AtomicIntegerという名前のnextIndexを使用して管理されます。これらのインデックスは、ThreadLocalから次のインデックスを取得してそれをインクリメントすることで初期化するnextIndexインスタンスを介してスレッドに割り当てられます。これは、各スレッドのインデックスが初めて取得されるときに発生します。 ThreadLocalは、このスレッドが作成した最後のシーケンスを追跡するために作成されます。 0に初期化されています。順次ファクトリオブジェクト参照が渡され、保存されます。サイズがAtomicReferenceArrayの2つのnインスタンスが作成されます。テールオブジェクトは各参照に割り当てられ、Sequentialファクトリによって提供される初期状態で初期化されています。 nは、許可されるスレッドの最大数です。これらの配列の各要素は、対応するスレッドインデックスに「属しています」。

適用方法:

これは面白い仕事をする方法です。次のことを行います。

  • この呼び出し用に新しいノードを作成します:mine
  • この新しいノードを、アナウンス配列の現在のスレッドのインデックスに設定します

次に、シーケンスループが始まります。現在の呼び出しがシーケンスされるまで続行されます。

  1. このスレッドによって作成された最後のノードのシーケンスを使用して、アナウンス配列内のノードを見つけます。これについては後で詳しく説明します。
  2. ステップ2でノードが見つかった場合、まだシーケンスされていません。続行します。それ以外の場合は、現在の呼び出しに焦点を当てます。これは、呼び出しごとに他の1つのノードのみを支援しようとします。
  3. 手順3で選択したノードが何であれ、最後にシーケンスされたノードの後に​​シーケンスを試行し続けます(他のスレッドが干渉する可能性があります)。成功に関係なく、decideNext()によって返されたシーケンスに現在のスレッドのヘッド参照を設定します

上記のネストされたループの鍵は、decideNext()メソッドです。それを理解するには、Nodeクラスを調べる必要があります。

ノードクラス

このクラスは、二重リンクリストのノードを指定します。このクラスではあまりアクションはありません。ほとんどのメソッドは、かなり自明であるはずの単純な取得メソッドです。

テール法

これは、シーケンスが0の特別なノードインスタンスを返します。呼び出しによって置き換えられるまで、これは単にプレースホルダーとして機能します。

プロパティと初期化

  • seq:シーケンス番号、-1に初期化(シーケンスなしを意味)
  • invocationapply()の呼び出しの値。建設時に設定します。
  • next:フォワードリンクの場合はAtomicReference。割り当てられると、これは決して変更されません
  • previous:シーケンス時に割り当てられ、truncate()によってクリアされた逆方向リンクのAtomicReference

次を決定

このメソッドはNodeの1つにすぎず、重要なロジックがあります。簡単に言えば、ノードはリンクリストの次のノードの候補として提供されます。 compareAndSet()メソッドは、参照がnullかどうかをチェックし、nullの場合は、参照を候補に設定します。参照がすでに設定されている場合は、何もしません。この操作はアトミックなので、2つの候補が同時に提供された場合、1つだけが選択されます。これにより、1つのノードのみが次のノードとして選択されることが保証されます。候補ノードが選択されると、そのシーケンスが次の値に設定され、前のリンクがこのノードに設定されます。

Universalクラスのapplyメソッドに戻ります...

ノードまたはannounce配列のノードを使用して、最後にシーケンスされたノード(チェックされている場合)でdecideNext()を呼び出した場合、2つの可能性があります。1.ノードが正常にシーケンスされた2.いくつか他のスレッドがこのスレッドを横取りしました。

次のステップは、この呼び出し用にノードが作成されたかどうかを確認することです。これは、このスレッドが正常にシーケンスしたか、他のスレッドがannounce配列から取得してシーケンスしたために発生する可能性があります。シーケンスされていない場合は、このプロセスが繰り返されます。それ以外の場合、呼び出しは、このスレッドのインデックスでのアナウンス配列をクリアし、呼び出しの結果値を返すことで終了します。アナウンス配列は、ノードのガベージコレクションの妨げとなるノードへの参照が残っていないことを保証するために消去され、リンクリスト内のすべてのノードがその時点からヒープ上に存続します。

評価方法

呼び出しのノードが正常にシーケンスされたので、呼び出しを評価する必要があります。これを行うには、最初のステップは、この呼び出しの前の呼び出しが評価されていることを確認することです。彼らがいない場合、このスレッドは待機しませんが、すぐに機能します。

EnsurePriorメソッド

ensurePrior()メソッドは、リンクリスト内の前のノードをチェックすることにより、この作業を行います。状態が設定されていない場合、前のノードが評価されます。 Nodeこれは再帰的です。前のノードの前のノードが評価されていない場合は、そのノードに対してevaluateを呼び出し、以下同様に続きます。

前のノードに状態があることがわかったので、このノードを評価できます。最後のノードが取得され、ローカル変数に割り当てられます。この参照がnullの場合、他のスレッドがこの参照を横取りし、このノードをすでに評価していることを意味します。状態を設定します。それ以外の場合、前のノードの状態は、このノードの呼び出しとともにSequentialオブジェクトのapplyメソッドに渡されます。返された状態がノードに設定され、truncate()メソッドが呼び出され、不要になったノードからの逆方向リンクがクリアされます。

MoveForwardメソッド

前方に移動するメソッドは、すべてのヘッド参照がこの先のノードをまだ指していなければ、このノードへの移動を試みます。これは、スレッドが呼び出しを停止した場合に、そのヘッドが、不要になったノードへの参照を保持しないようにするためです。 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 1Thread 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 2announce[0](6 mod 3)を指すようになりました。 3つのスレッドはすべて、同じ呼び出しのシーケンスを試みるように設定されています。どのスレッドが成功しても、次にシーケンスされるノードは、スレッド1の待機中の呼び出しになります。つまり、announce[0]によって参照されるノードです。

これは避けられません。スレッドがプリエンプトされるためには、他のスレッドがノードをシーケンスしている必要があり、そうすることで、スレッドは継続的にlastSequenceを先に進めます。特定のスレッドのノードが継続的にシーケンスされない場合、最終的にすべてのスレッドは、アナウンス配列のそのインデックスを指します。支援しようとしているノードがシーケンス処理されるまで、スレッドは何もしません。最悪のシナリオは、すべてのスレッドが同じシーケンス処理されていないノードを指している場合です。したがって、呼び出しをシーケンス処理するために必要な時間は、スレッドの数の関数であり、入力のサイズではありません。

1
JimmyJames

私の以前の答えは実際には質問に適切に答えていませんが、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));
    }
  }
}

ここに投稿するときに、コードをいくつか編集しました。大丈夫ですが、問題がある場合はお知らせください。

0
JimmyJames