22

更新:これが Hashed Timing Wheels の私の実装です。パフォーマンスと同時実行性を改善するアイデアがあれば教えてください。(2009 年 1 月 20 日)

// Sample usage:
public static void main(String[] args) throws Exception {
    Timer timer = new HashedWheelTimer();
    for (int i = 0; i < 100000; i ++) {
        timer.newTimeout(new TimerTask() {
            public void run(Timeout timeout) throws Exception {
                // Extend another second.
                timeout.extend();
            }
        }, 1000, TimeUnit.MILLISECONDS);
    }
}

更新: Hierarchical and Hashed Timing Wheelsを使用してこの問題を解決しました。(2009 年 1 月 19 日)

タイムアウト処理用に最適化された特別な目的のタイマーを Java で実装しようとしています。たとえば、ユーザーは期限のあるタスクを登録でき、タイマーは期限が過ぎたときにユーザーのコールバック メソッドに通知できます。ほとんどの場合、登録されたタスクは非常に短い時間内に完了するため、ほとんどのタスクはキャンセルされるか (例: task.cancel())、将来に再スケジュールされます (例: task.rescheduleToLater(1, TimeUnit.SECOND))。 .

このタイマーを使用して、アイドル状態のソケット接続を検出し (たとえば、10 秒以内にメッセージが受信されない場合に接続を閉じる)、書き込みタイムアウトを検出します (たとえば、書き込み操作が 30 秒以内に終了しない場合に例外を発生させます)。異常なネットワークの問題がない限り、タイムアウトは発生せず、クライアントはメッセージを送信し、応答が送信されます。

java.util.Timer または java.util.concurrent.ScheduledThreadPoolExecutor は、ほとんどのタスクがタイムアウトになると想定しているため、使用できません。タスクがキャンセルされた場合、キャンセルされたタスクは、ScheduledThreadPoolExecutor.purge() が呼び出されるまで内部ヒープに格納されます。これは非常にコストのかかる操作です。(O(NlogN) もしかして?)

CSクラスで学んだ従来のヒープまたは優先度キューでは、要素の優先度の更新は高価な操作でした(多くの場合、要素を削除して再挿入することによってのみ達成できるため、O(logN)新しい優先度値. フィボナッチヒープのようないくつかのヒープには、reduceKey() と min() 操作の O(1) 時間がありますが、少なくとも私が必要としているのは、高速な increaseKey() と min() (または reduceKey() と max()) です。 .

この特定のユースケースに対して高度に最適化されたデータ構造を知っていますか? 私が考えている戦略の 1 つは、すべてのタスクをハッシュ テーブルに保存し、すべてのタスクを 1 秒ごとに反復処理することですが、それほど美しいものではありません。

4

9 に答える 9

13

早く終わる通常の場合とエラーの場合の扱いを分けてみてはいかがでしょうか。

ハッシュ テーブルとプライオリティ キューの両方を使用します。タスクが開始されるとハッシュ テーブルに入れられ、すぐに終了すると O(1) 時間で削除されます。

1 秒ごとにハッシュ テーブルをスキャンし、長時間 (たとえば 0.75 秒) 経過したタスクを優先キューに移動します。プライオリティ キューは常に小さく、扱いやすいものにする必要があります。これは、1 秒が探しているタイムアウト時間よりもはるかに短いことを前提としています。

ハッシュ テーブルのスキャンが遅すぎる場合は、基本的に 1 つは偶数秒用、もう 1 つは奇数秒用の 2 つのハッシュ テーブルを使用できます。タスクが開始されると、現在のハッシュ テーブルに配置されます。毎秒すべてのタスクを非現行ハッシュ テーブルから優先度キューに移動し、ハッシュ テーブルを交換して、現行ハッシュ テーブルが空になり、非現行テーブルに 1 ~ 2 秒前に開始されたタスクが含まれるようにします。

優先度キューを使用するよりもはるかに複雑なオプションがありますが、非常に簡単に実装でき、安定している必要があります。

于 2009-01-17T15:31:25.173 に答える
11

私の知る限りでは (私は新しい優先度キューに関する論文を書き、過去の結果もレビューしました)、優先度キューの実装はフィボナッチ ヒープの境界や一定時間増加キーを取得しません。

文字通りそれを得るには小さな問題があります。O(1) で増加キーを取得できる場合は、O(1) で削除を取得できます。キーを +infinity に増やすだけです (いくつかの標準的な償却トリックを使用して、多数の +infinity でいっぱいになっているキューを処理できます)。 )。ただし、find-min も O(1) の場合、delete-min = find-min + delete は O(1) になります。比較ベースの優先度キューでは不可能です。なぜなら、並べ替えの境界が (すべてを挿入してから 1 つずつ削除する) ことを意味するからです。

n * 挿入 + n * 最小削除 > n ログ n.

ここでのポイントは、O(1) で優先度キューが増加キーをサポートするようにしたい場合は、次のペナルティのいずれかを受け入れる必要があるということです。

  • 比較に基づくものではありません。実際、これはvEB ツリーなどを回避するためのかなり良い方法です。
  • 挿入には O(log n) を、make-heap には O(n log n) を受け入れます (n 開始値を指定)。これはひどい。
  • find-min に O(log n) を受け入れます。実際に find-min を実行しない場合 (削除を伴わずに)、これはまったく問題ありません。

しかし、繰り返しになりますが、私の知る限り、最後の選択肢を実行した人はいません。私はこれを、データ構造の非常に基本的な領域で新しい結果を得るチャンスだと常に考えてきました。

于 2009-01-19T06:51:06.763 に答える
6

詳細については、ハッシュタイミングホイールを使用してください-Googleの「ハッシュ階層タイミングホイール」。これは、ここの人々によってなされた答えの一般化です。階層的なタイミングホイールよりも、ホイールサイズが大きいハッシュ化されたタイミングホイールの方が好きです。

于 2009-01-19T06:23:35.897 に答える
5

ハッシュと O(logN) 構造のいくつかの組み合わせは、あなたが求めていることを行うはずです。

あなたが問題を分析している方法について、私は口論したくなります。上記のコメントで、あなたは言います

更新は非常に頻繁に行われるためです。接続ごとに M メッセージを送信すると、全体の時間は O(MNlogN) になり、これはかなり大きいとします。– Trustin Lee (6 時間前)

それが行く限り、これは絶対に正しいです。しかし、私が知っているほとんどの人は、メッセージごとのコストに集中し、アプリで行う作業が増えるにつれて、明らかにより多くのリソースが必要になるという理論に基づいています。

したがって、アプリケーションで 10 億個のソケットが同時に開かれている場合 (それは本当に可能性が高いでしょうか?)、挿入コストはメッセージごとに約 60 回の比較にすぎません。

これは時期尚早の最適化であると断言します。CodeAnalyst や VTune などのパフォーマンス分析ツールを使用して、システムのボトルネックを実際に測定したことはありません。

とにかく、単一の構造では目的を達成できないと判断し、さまざまなアルゴリズムの長所と短所の組み合わせが必要になると判断した場合、おそらく無限の数の方法があります。

1 つの可能性は、ソケット ドメイン N をサイズ B のいくつかのバケットに分割し、各ソケットをそれらの (N/B) バケットの 1 つにハッシュすることです。そのバケットには、更新時間が O(log B) のヒープ (または何でも) があります。N の上限が事前に固定されておらず、変化する可能性がある場合は、より多くのバケットを動的に作成できます。これにより少し複雑になりますが、確実に実行可能です。

最悪の場合、ウォッチドッグ タイマーは (N/B) キューの有効期限を検索する必要があります。 つまり、最後のタイム スライスで 10 個のソケットがアイドル状態になった場合、そのドメインで最初にタイムアウトしたドメインを検索し、それに対処してから、2 番目にタイムアウトしたドメインを見つける、というように行う必要はありません。 (N/B) 個のバケット セットをスキャンし、すべてのタイムアウトを列挙する必要があります。

バケットの線形配列に満足できない場合は、キューの優先キューを使用できますが、すべてのメッセージでそのキューを更新することは避けたいと考えています。代わりに、実際のタイムアウトより短い時間を定義してください。(たとえば、その 3/4 または 7/8) そして、最長時間がそれを超える場合にのみ、低レベルのキューを高レベルのキューに入れます。

そして、明白なことを述べるリスクがありますが、経過時間に基づいてキューをキーにしたくありません。キーは開始時間にする必要があります。キュー内のレコードごとに、経過時間を常に更新する必要がありますが、各レコードの開始時刻は変わりません。

于 2009-01-17T22:14:03.583 に答える
3

O(1) ですべての挿入と削除を行う非常に簡単な方法があります。これは、1) 優先度が時間に基づいていること、および 2) おそらく少数の固定数のタイムアウト期間があるという事実を利用したものです。

  1. 通常の FIFO キューを作成して、10 秒でタイムアウトするすべてのタスクを保持します。すべてのタスクのタイムアウト期間は同じであるため、最後に挿入して最初から削除するだけで、キューをソートしたままにすることができます。
  2. タイムアウト期間が 30 秒のタスク用に別の FIFO キューを作成します。他のタイムアウト期間用にさらにキューを作成します。
  3. キャンセルするには、アイテムをキューから削除します。キューがリンクされたリストとして実装されている場合、これは O(1) です。
  4. どちらの操作も O(1) であるため、再スケジュールはキャンセル挿入として実行できます。タスクは別のキューに再スケジュールできることに注意してください。
  5. 最後に、すべての FIFO キューを 1 つの全体的な優先度キューに結合するには、すべての FIFO キューの先頭を通常のヒープに参加させます。このヒープの先頭は、すべてのタスクの中でタイムアウトが最も早く期限切れになるタスクになります。

m 個の異なるタイムアウト期間がある場合、構造全体の各操作の複雑さは O(log m) です。挿入先のキューを検索する必要があるため、挿入は O(log m) です。ヒープを復元するための remove-min は O(log m) です。キャンセルは O(1) ですが、キューの先頭をキャンセルする場合は最悪の場合 O(log m) になります。m は小さい固定数であるため、O(log m) は本質的に O(1) です。タスクの数に比例しません。

于 2012-03-22T03:23:54.557 に答える
2

あなたの特定のシナリオは、循環バッファを私に示唆しています。最大の場合。timeout は 30 秒で、少なくとも 10 分の 1 秒ごとにソケットをリープし、300 個の双方向リンク リストのバッファを使用します。エントリを 'increaseTime' するには、そのエントリが含まれているリストから削除し、新しい 10 秒間隔のエントリに追加します (どちらも一定時間の操作)。ピリオドが終了したら、現在のリストに残っているものをすべて取得し (おそらくリーパー スレッドにフィードすることによって)、現在のリスト ポインターを進めます。

于 2009-01-17T23:28:57.490 に答える
0

キュー内のアイテム数にハードリミットがあります - TCP ソケットに制限があります。

したがって、問題は限定的です。巧妙なデータ構造は、組み込み型を使用するよりも遅くなると思います。

于 2009-01-16T13:25:26.473 に答える
0

すべてのタスクをリストに保存し、それらを反復するのが最善だと思います。

このコストが重要になる限界に到達するには、かなり強力なマシンでサーバーを実行する必要がありますか?

于 2009-01-17T09:14:04.610 に答える
0

java.lang.PriorityQueue を使用しない正当な理由はありますか? remove() は log(N) 時間でキャンセル操作を処理しませんか? 次に、キューの先頭にあるアイテムまでの時間に基づいて、独自の待機を実装します。

于 2009-01-16T15:14:18.940 に答える