6

次のコードを書くことが悪い習慣と見なされる理由は何ですか?

  while (someList.isEmpty()) {
    try {
      Thread.currentThread().sleep(100);
    }
    catch (Exception e) {}
  }
  // Do something to the list as soon as some thread adds an element to it.

私にとって、sleep に任意の値を選択することは良い習慣ではなくBlockingQueue、この状況では a を使用しますが、そのようなコードを記述すべきではない理由が複数あるかどうかを知りたいです。

4

6 に答える 6

6

イベントが処理される前に平均 50 ミリ秒の遅延が発生し、処理するイベントがない場合は 1 秒間に 10 回起動します。どちらも特に重要でない場合、それは単にエレガントではありません。

于 2012-01-12T07:13:28.517 に答える
1

ループは、すべきでないことの優れた例です。;)


Thread.currentThread().sleep(100);

これは静的メソッドであるため、currentThread()を取得する必要はありません。と同じです

Thread.sleep(100);

catch (Exception e) {}

これは非常に悪い習慣です。誰かがコードをコピーする可能性があるので、これを例に入れてもお勧めしません。このフォーラムの質問のかなりの部分は、与えられた例外を印刷して読むことによって解決されます。


You don't need to busy wait here. esp. when you expect to be waiting for such a long time.  Busy waiting can make sense if you expect to be waiting a very very short amount of time. e.g.

// From AtomicInteger
public final int getAndSet(int newValue) {
    for (;;) {
        int current = get();
        if (compareAndSet(current, newValue))
            return current;
    }
}

ご覧のとおり、このループが複数回周回する必要があることは非常にまれであり、指数関数的に何度も周回する可能性は低くなります。(実際のアプリケーションでは、マイクロベンチマークではありません)このループは10 nsと短い場合がありますが、これは長い遅延ではありません。


不必要に99ミリ秒待つ可能性があります。プロデューサーが1ミリ秒後にエントリを追加しているとすると、何も待たずに長い間待機していました。

解決策はより単純で明確です。

BlockingQueue<E> queue = 

E e = queue.take(); // blocks until an element is ready.

リスト/キューは別のスレッドでのみ変更され、スレッドとキューを管理するためのはるかに単純なモデルは、ExecutorServiceを使用することです。

ExecutorService es =

final E e = 
es.submit(new Runnable() {
   public void run() {
       doSomethingWith(e);
   }
});

ご覧のとおり、キューやスレッドを直接操作する必要はありません。スレッドプールに何をさせたいかを言う必要があります。

于 2012-01-12T08:18:29.983 に答える
1

これを行わない理由はたくさんあります。まず、お気づきのように、これは、スレッドがスリープしている可能性があるため、スレッドが応答する必要があるイベントが発生してから実際の応答時間までに大きな遅延が生じる可能性があることを意味します。第 2 に、どのシステムにも非常に多くの異なるプロセッサしかないため、重要なスレッドをプロセッサから追い出し、別の機会にスレッドにスリープ状態に移行するよう指示できるようにしなければならない場合、システムによって実行される有用な作業の総量が減少します。システムの電力使用量を増やします (これは、電話や組み込みデバイスなどのシステムで重要です)。

于 2012-01-12T07:14:05.093 に答える
0

他の回答に追加するには、キューからアイテムを削除するスレッドが複数ある場合、競合状態もあります。

  1. キューは空です
  2. スレッド A が要素をキューに入れる
  3. スレッド B は、キューが空かどうかをチェックします。そうではない
  4. スレッド C は、キューが空かどうかをチェックします。そうではない
  5. スレッド B はキューから取得します。成功
  6. スレッド C はキューから取得します。失敗

synchronizedキューが空かどうかを(ブロック内で) アトミックにチェックし、そうでない場合はそこから要素を取得することで、これを処理できます。今あなたのループはただの醜いものに見えます:

T item;
while ( (item = tryTake(someList)) == null) {
    try {
        Thread.currentThread().sleep(100);
    }
    catch (InterruptedException e) {
        // it's almost never a good idea to ignore these; need to handle somehow
    }
}
// Do something with the item

synchronized private T tryTake(List<? extends T> from) {
    if (from.isEmpty()) return null;
    T result = from.remove(0);
    assert result != null : "list may not contain nulls, which is unfortunate"
    return result;
}

または、を使用することもできますBlockingQueue

于 2012-01-12T07:27:11.063 に答える
0

また、クラスに競合状態を導入します。通常のリストの代わりにブロッキング キューを使用していた場合、スレッドはリストに新しいエントリができるまでブロックされます。あなたの場合、ワーカースレッドがスリープしている間に2番目のスレッドがリストから要素を取得および取得する可能性があり、気付かないでしょう。

于 2012-01-12T07:24:26.010 に答える
0

David、templatetypedefなどによって提供された優れた回答に直接追加することはできません-スレッド間通信の遅延とリソースの浪費を避けたい場合は、sleep()ループでスレッド間通信を行わないでください。

先制スケジューリング/ディスパッチ:

CPU レベルでは、割り込みが重要です。OS は、そのコードが入力される原因となる割り込みが発生するまで何もしません。OS 用語では、割り込みには 2 つのフレーバーがあることに注意してください。ドライバーを実行させる「実際の」ハードウェア割り込みと「ソフトウェア割り込み」です。これらは、実行中のスレッドのセットを潜在的に引き起こす可能性がある、既に実行中のスレッドからの OS システム コールです。変更する。キープレス、マウスの動き、ネットワーク カード、ディスク、ページ フォールトはすべて、ハードウェア割り込みを生成します。待機関数とシグナル関数、および sleep() は、その 2 番目のカテゴリに属します。ハードウェア割り込みによってドライバーが実行されると、ドライバーは設計されたハードウェア管理を実行します。ドライバーが OS にスレッドを実行する必要があることを通知する必要がある場合 (おそらく、ディスク バッファーがいっぱいになり、処理する必要があります)、

上記の例のような割り込みは、待機していたスレッドを実行できるようにするか、実行中のスレッドを待機状態にすることができます。割り込みのコードを処理した後、OS はそのスケジューリング アルゴリズムを適用して、割り込みの前に実行されていたスレッドのセットが、現在実行されるべきスレッドのセットと同じかどうかを判断します。そうである場合、OS は単に割り込みを返します。そうでない場合、OS は実行中のスレッドの 1 つ以上を横取りする必要があります。割り込みを処理したものではない CPU コアで実行されているスレッドを OS が横取りする必要がある場合、OS はその CPU コアの制御を取得する必要があります。これは、「実際の」ハードウェア割り込みによって行われます。OS のプロセッサ間ドライバーは、プリエンプトされるスレッドを実行しているコアをハード割り込みするハードウェア信号を設定します。

プリエンプトされるスレッドが OS コードに入ると、OS はそのスレッドの完全なコンテキストを保存できます。一部のレジスタは、割り込みエントリによってスレッドのスタックに既に保存されているため、スレッドのスタックポインタを保存すると、これらすべてのレジスタが効果的に「保存」されますが、通常、OS はさらに多くのことを行う必要があります。例えば。キャッシュをフラッシュする必要がある場合、FPU の状態を保存する必要がある場合があります。実行する新しいスレッドがプリエンプトされるプロセスとは異なるプロセスに属している場合は、メモリ管理保護レジスタをスワップアウトする必要があります。 . 通常、OS は、割り込みスレッド スタックからプライベート OS スタックにできるだけ早く切り替えて、すべてのスレッド スタックに OS スタック要件が課されるのを回避します。

コンテキストが保存されると、OS は、実行される新しいスレッドの拡張コンテキストを「スワップイン」できます。これで、OS は最終的に新しいスレッドのスタック ポインターをロードし、割り込みリターンを実行して、新しい準備ができたスレッドを実行できるようになります。

その後、OS は何もしません。実行中のスレッドは、別の割り込み (ハードまたはソフト) が発生するまで実行されます。

重要なポイント:

1) OS カーネルは、中断されたスレッドとは異なる一連のスレッドに割り込みを返すことを決定できる大きな割り込みハンドラーと見なす必要があります。

2) OS は、スレッドの状態や実行中のコアに関係なく、任意のプロセスの任意のスレッドを制御し、必要に応じて停止できます。

3) プリエンプティブなスケジューリングとディスパッチは、これらのフォーラムに投稿されているすべての同期などの問題を生成します。大きな利点は、ハード割り込みに対するスレッドレベルでの応答が速いことです。これがなければ、PC で実行するすべての高性能アプリ (ビデオ ストリーミング、高速ネットワークなど) は事実上不可能になります。

4) OS タイマーは、実行中のスレッドのセットを変更できる大きな割り込みセットの 1 つにすぎません。「タイムスライシング」(うーん、私はその用語が嫌いです)、準備ができているスレッド間で、コンピューターが過負荷になっている場合にのみ発生します。準備ができているスレッドのセットは、それらを実行するために使用できる CPU コアの数よりも大きくなっています。OS のスケジューリングを説明するテキストで、「割り込み」の前に「タイム スライス」に言及している場合、説明よりも混乱を招く可能性があります。タイマー割り込みは、多くのシステム コールが主要な機能をバックアップするためのタイムアウトを持っているという点で「特別」です (OK、sleep() の場合、タイムアウトは主要な機能です:)。

于 2012-01-12T11:20:37.317 に答える