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


16

「ユニバーサルコンストラクション」は、シーケンシャルオブジェクトの線形化を可能にするラッパークラスです(同時オブジェクトの強力な整合性条件)。たとえば、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インターフェイスを拡張できる方法はありますか?

wait-freeプロパティを失うことなく、これをより効率的にすることができます(履歴サイズの線形ランタイムではなく、できればメモリ使用量も減少します)。

明確化

「ユニバーサル構造」とは、[1]で構成されていると確信している用語です。[1]は、スレッドセーフではないが、スレッド互換のオブジェクトを受け入れSequentialます。待機なしのキューを使用して、最初の構築では、スレッドセーフで線形化可能なバージョンのオブジェクトを提供しますapply

メソッドは、各ローカルスレッドをクリーンな状態から開始し、これまでに記録されたすべての操作を適用するのが効果的であるため、これは非効率的です。いずれにせよ、この作品には、使用して効果的な同期を実現するため、WFQすべてのスレッドの呼び出しは:すべての操作を適用する順序を決定するためにapply同じローカル表示されますSequentialのと同じ順序で、オブジェクトをInvocationsがそれに適用します。

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

専門用語:

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

要求された実際の例(現在は期限切れにならないページにあります)

[1] Herlihy and Shavit、The Art of Multiprocessor Programming


質問1は、「動作」があなたにとって何を意味するかを知っている場合にのみ答えられます。
ロバートハーヴェイ

@RobertHarvey私はそれを修正しました-「作業」するために必要なのは、ラッパーが待機なしであり、すべての操作CopyableSequentialが有効であるためです-線形化可能性は、それがであるという事実に従う必要がありますSequential
VF1

この質問には意味のある言葉がたくさんありますが、あなたが何を達成しようとしているかを正確に理解するためにそれらをまとめるのに苦労しています。解決しようとしている問題の説明を提供できますか。
ジミージェームズ

@JimmyJames質問内の「拡張コメント」で詳しく説明しました。他に解決すべき専門用語があれば教えてください。
VF1

コメントの最初の段落では、「スレッドに対して安全ではないがスレッド互換のオブジェクト」および「オブジェクトの線形化可能なバージョン」と言います。スレッドセーフ線形化できるのは実行可能命令にのみ真に関連しているが、それを使用してデータであるオブジェクトを記述しているため、それがどういう意味かは不明です。私はと推測呼び出し(定義されていない)を効果的にメソッドポインタであり、それはスレッドセーフではないという方法です。スレッド互換性の意味がわかりません。
ジミージェームズ

回答:


1

これがどのように達成されるかの説明と例があります。明確でない部分があるかどうかを教えてください。

ソースの要点

ユニバーサル

初期化:

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

適用方法:

これは興味深い仕事をする方法です。次のことを行います。

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

その後、シーケンスループが開始されます。現在の呼び出しがシーケンス化されるまで続行されます。

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

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

ノードクラス

このクラスは、二重リンクリストのノードを指定します。このクラスではそれほど多くのアクションはありません。ほとんどのメソッドは、簡単に取得できるメソッドであり、一目瞭然です。

テール方式

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

プロパティと初期化

  • seq:-1に初期化されたシーケンス番号(シーケンスなしを意味する)
  • invocation:の呼び出しの値apply()。建設時に設定します。
  • nextAtomicReference前方リンク用。一度割り当てられると、これは変更されません
  • previousAtomicReferenceシーケンス時に割り当てられ、クリアされた逆方向リンク用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は呼び出しで実行されます。アナウンス配列の最後のシーケンス(ゼロ)を確認します。これは、現在インデックスを作成するようにスケジュールされているノードです。ノードをシーケンスし、lastSequence1に設定されます。

スレッド2は呼び出しで実行されるようになりました。最後のシーケンス(ゼロ)でアナウンス配列をチェックし、ヘルプを必要としないため、呼び出しのシーケンスを試みます。成功し、現在lastSequenceは2に設定されています。

スレッド3が実行され、ノードannounce[0]が既にシーケンス化されていることと、独自の呼び出しをシーケンス化することも確認されます。現在lastSequenceは3に設定されています。

これで、スレッド1が再び呼び出されます。インデックス1のアナウンス配列をチェックし、既に配列されていることを見つけます。同時に、スレッド2が呼び出されます。インデックス2のアナウンス配列をチェックし、既に配列されていることを検出します。スレッド1スレッド2の両方が、独自のノードのシーケンスを試行するようになりました。 スレッド2が勝ち、その呼び出しをシーケンスします。それはだlastSequence4一方に設定されている、スレッド3が呼び出されました。インデックスit lastSequence(mod 3)をチェックし、ノードannounce[0]がシーケンスされていないことを検出します。 スレッド2は、スレッド1が2回目の試行中に同時に呼び出されます。 スレッド1スレッド2announce[1]によって作成されたばかりのノードである、シーケンスなしの呼び出しを見つけます。スレッド2の呼び出しのシーケンスを試み、成功します。 スレッド2は自身のノードを見つけ、シーケンスされています。5に設定されます。 次に、スレッド3が呼び出され、スレッド1が配置されたノードがまだシーケンスされていないことが検出され、シーケンスが試行されます。一方、スレッド2も呼び出され、スレッド3をプリエンプション処理します。ノードをシーケンスし、6に設定します。announce[1]lastSequenceannounce[0]lastSequence

悪いスレッド1。にもかかわらず、スレッド3は、それをシーケンスしようとしている、両方のスレッドは継続的にスケジューラによって阻止されています。しかし、この時点で。スレッド2announce[0](6 mod 3)を指しています。3つのスレッドはすべて、同じ呼び出しをシーケンスするように設定されています。どのスレッドが成功しても、シーケンスされる次のノードは、スレッド1の待機中の呼び出し、つまりによって参照されるノードになりますannounce[0]

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


pastebinにコードの抜粋をいくつか入れていただけますか?多くのこと(ロックフリーリンクリストなど)は、単純にそのように言うことができますか?非常に多くの詳細がある場合、全体としてあなたの答えを理解することは少し難しいです。いずれにせよ、これは有望に見えます。私は確かにそれが提供する保証を掘り下げたいと思います。
VF1

これは確かに有効なロックフリー実装のように見えますが、私が心配している根本的な問題がありません。線形化可能性の要件により、「有効な履歴」が存在する必要がpreviousありnextます。これは、リンクリスト実装の場合、有効にするためにand ポインターを必要とします。有効な履歴を待機なしで維持および作成するのは難しいようです。
VF1

@ VF1どの問題に対処していないのかわかりません。コメントの残りの部分で言及するすべてのものは、私が伝えることができるものから、私が与えた例で扱われています。
ジミージェームズ

ウェイトフリープロパティを放棄しました。
VF1

@ VF1どう思いますか?
ジミージェームズ

0

私の以前の答えは実際に質問に適切に答えていませんが、OPがそれを有用であると考えているので、私はそれをそのままにします。質問のリンク内のコードに基づいて、ここに私の試みがあります。私はこれに関して本当に基本的なテストのみを行いましたが、平均を適切に計算しているようです。これが適切に待機フリーであるかどうかについてのフィードバックを歓迎します。

:ユニバーサルインターフェイスを削除し、クラスにしました。ユニバーサルをシーケンシャルで構成するだけでなく、シーケンシャルで構成することは不要な複雑さのように思えますが、何かが欠けている可能性があります。平均的なクラスでは、状態変数をにマークしました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));
    }
  }
}

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


あなたは私のために他の答えを維持する必要はありません(関連する結論を引き出すために私の質問を以前に更新しました)。残念ながら、この答えは質問に答えません。実際にのメモリを解放しないwfqため、履歴全体をたどる必要があります-ランタイムは一定の要因を除いて改善されていません。
VF1

@ Vf1リストが計算されたかどうかをチェックするためにリスト全体を走査するのにかかる時間は、各計算を実行するのに比べてわずかです。前の状態は必須ではないため、初期状態を削除することは可能です。テストは難しく、カスタマイズされたコレクションを使用する必要があるかもしれませんが、小さな変更を加えました。
ジミージェームズ16

@ VF1基本的な大まかなテストで動作するように見える実装に更新されました。安全であるかどうかはわかりませんが、頭の上の方では、ユニバーサルが動作しているスレッドを認識していれば、各スレッドを追跡し、すべてのスレッドが安全に過ぎたら要素を削除できます。
ジミージェームズ16

@ VF1 ConcurrentLinkedQueueのコードを見ると、offerメソッドには、他の回答を待機なしにしたと主張したループによく似ています。「別のスレッドに失われたCASレース、再読み込み、次の」コメントのためのルック
JimmyJames

「初期状態を削除することができるはずです」-正確に。であるべきですが、待機の自由を失うコードを微妙に導入するのは簡単です。スレッド追跡スキームが機能する場合があります。最後に、CLQソースにアクセスできません。リンクしてくれませんか?
VF1
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.