9

私は以下のコードに出くわしました、そしてそれが私が思うことを正確に行うかどうか疑問に思っています:

synchronized(sObject) {
    mShouldExit = true;   
    sObject.notifyAll()    
    while (!mExited) {
      try {
           sObject.wait();
        } catch (InterruptedException ex) {
           Thread.currentThread().interrupt();
        }
     }
}

mShouldExitコンテキストについて:(モニター内で)チェックしsObject、その場合に終了する別のスレッドがあります。

これは私には正しいパターンではないようです。割り込みが発生すると、割り込みステータスが再度設定されるため、に戻るとsObject.wait()、別のInterruptedExceptionが発生するなどの問題が発生します。したがって、真の待機状態()に移行することはできません。sObject.wait()つまり、sObjectモニターを解放することはありません。他のスレッドはsObjectのモニターに入ることができないため、mExitingをtrueに設定できないため、これにより無限ループが発生する可能性があります。(したがって、この呼び出しはエラーだと思いinterrupt()ます。ここでは使用しないでください。)何かが足りないのでしょうか。

コードスニペットは、公式のAndroidフレームワークソースコードの一部であることに注意してください。

更新:実際には、GLレンダリングの開始時にAndroidで同じパターンが使用されるため、状況はさらに悪化します。の公式ソースコードGLSurfaceView.GLThread.surfaceCreated()

   public void surfaceCreated() {
        synchronized(sGLThreadManager) {
            if (LOG_THREADS) {
                Log.i("GLThread", "surfaceCreated tid=" + getId());
            }
            mHasSurface = true;
            sGLThreadManager.notifyAll();
            while((mWaitingForSurface) && (!mExited)) {
                try {
                    sGLThreadManager.wait();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }

同様の方法でバグを再現できます。UIスレッドに割り込みステータスフラグがまだあることを確認してから、GLSurfaceViewを追加してGLレンダリングを開始します(setRenderer(...)ただし、一部のデバイスでは、GLSurfaceViewにVisibility.VISIBLEステータスがあることを確認してください。そうでない場合、レンダリングは行われません。始める)。

上記の手順に従うと、UIスレッドは無限ループになります。これは、上記のコードがInterruptedException(のために)を生成し続けるため、GLスレッドがfalsewait()に設定されることはないためです。mWaitingForSurface

私のテストによると、このような無限ループは、GC_CONCURRENTガベージコレクション(または、少なくともlogcat内のこのようなメッセージ)の無限シーケンスをもたらすようです。興味深いことに、誰かが以前にスタックオーバーフローに関する未知の不十分に定義された問題を抱えていました。これは関連している可能性があります: 解放されたGC_concurrentを解決する方法は?

彼のUIスレッドの割り込みフラグがtrueに設定されていて、彼が言及しているマップにGLSurfaceViewを使用していた可能性はありませんか?単なる仮定、考えられるシナリオ。

4

1 に答える 1

17

短いバージョン:そのコードは間違っており、無限ループが発生します(まだ疑問がありますが、JVMの実装に依存する可能性があります)。割り込みステータスを設定するのは正しいことですが、ループを終了し、最終的にThread.isInterrupted()を使用して同じ割り込みステータスをチェックする必要があります。

カジュアルリーダー向けのロングバージョン:

問題は、ユーザーからの「キャンセル」ボタンに応答して、または他のアプリケーションロジックのために、現在何らかの作業を実行しているスレッドを停止する方法です。

当初、Javaは、スレッドをプリエンプティブに停止する「stop」メソッドをサポートしていました。この方法は安全ではないことが実証されています。原因は、停止したスレッドに、クリーンアップ、リソースの解放、部分的に変更されたオブジェクトの公開を回避するなどの(簡単な)方法を提供しませんでした。

そのため、Javaは「協調的」スレッド「中断」システムに進化しました。このシステムは非常に単純です。スレッドが実行され、他の誰かが「割り込み」を呼び出し、スレッドにフラグが設定されます。スレッドが中断されたかどうかを確認し、それに応じて動作するのはスレッドの責任です。

したがって、正しいThread.run(またはCallableなどのRunnable.run)メソッドの実装は次のようになります。

public void run() {
  while (!Thread.getCurrentThread().isInterrupted()) {
    // Do your work here
    // Eventually check isInterrupted again before long running computations
  }
  // clean up and return
}

スレッドが実行しているすべてのコードがrunメソッド内にあり、長時間ブロックするものを呼び出さない限り、これは問題ありません...多くの場合、そうではありません。スレッドを生成する場合は、やることが長い。

ブロックする最も簡単なメソッドはThread.sleep(millis)であり、実際にはそれが唯一のことです。指定された時間、スレッドをブロックします。

これで、スレッドがThread.sleep(600000000)内にあるときに割り込みが到着した場合、他のサポートがなければ、isInterruptedをチェックするポイントに到達するまでに多くの時間がかかります。

スレッドが決して終了しない状況もあります。たとえば、スレッドが何かを計算し、制限されたサイズのBlockingQueueに結果を送信している場合、queue.put(myresult)を呼び出します。その間にコンシューマーがキュー内のスペースを解放するまで、スレッドはブロックします。中断された(または死んだなど)、そのスペースは決して到着せず、メソッドは戻らず、.isInterruptedのチェックは実行されず、スレッドはスタックします。

この状況を回避するために、スレッドを中断するすべての(ほとんどの)メソッドはInterruptedExceptionをスローする(すべきです)。その例外は、「私はこれとあれを待っていましたが、その間にスレッドが中断されたので、クリーンアップを実行してできるだけ早く終了する必要があります」と単純に伝えます。

すべての例外と同様に、何をすべきかわからない限り、それを再スローして、コールスタックの上位の誰かが知っていることを期待する必要があります。

InterruptedExceptionsがスローされると、「interrupted status」がクリアされるため、InterruptedExceptionsはさらに悪化します。これは、単にそれらをキャッチして無視すると、通常は停止しないスレッドになることを意味します。

public void run() {
  while (!Thread.getCurrentThread().isInterrupted()) {
    try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      // Nothing here
    }
  }
}

この例では、sleep()メソッド(時間の99.9999999999%)中に割り込みが到着すると、InterruptedExceptionがスローされ、割り込みフラグがクリアされます。その後、割り込みフラグがfalseであるため、ループが続行され、スレッドは止まらない。

そのため、.isInterruptedを使用して「while」を正しく実装し、InterruptedExceptionを実際にキャッチする必要があり、それを処理するための特別なこと(クリーンアップ、リターンなど)がない場合は、少なくとも実行できます。割り込みフラグが再度設定されます。

投稿したコードの問題は、「while」がmExitedのみに依存して停止するタイミングを決定し、isInterruptedにも依存しないことです。

while (!mExited && !Thread.getCurrentThread().isInterrupted()) {

または、中断されたときに終了する可能性があります:

} catch (InterruptedException e) {
  Thread.currentThread().interrupt();
  return; // supposing there is no cleanup or other stuff to be done
}

スレッドを制御しない場合は、isInterruptedフラグをtrueに戻すことも重要です。たとえば、ある種のスレッドプールで実行されているランナブルにいる場合、またはスレッドを所有および制御していないメソッド(単純なケース:サーブレット)内にいる場合は、中断は「あなた」(サーブレットの場合、クライアントが接続を閉じ、コンテナが他の要求のためにスレッドを解放しようとしている)、またはスレッド(またはシステム)全体を対象としている場合(コンテナがシャットダウンし、すべてが停止しています)。

その状況(コードの99%)で、InterruptedException(残念ながらチェックされています)を再スローできない場合、スレッドが中断されたスレッドプールにスタックを伝播する唯一の方法は、戻る前にtrueにフラグを戻します。

そうすることで、スタックを伝播し、最終的には、適切に反応できるスレッド所有者(jvm自体、Executor、またはその他のスレッドプール)まで、より多くのInterruptedExceptionを生成します(スレッドを再利用し、終了させます、 System.exit(1)...)

このほとんどは、Java Concurrency in Practiceの第7章で説明されています。これは、Javaだけでなく、コンピュータプログラミング全般に関心のある人におすすめする非常に優れた本で、他の多くの環境でも問題と解決策が似ています。非常によく書かれています。

SunがInterruptedExceptionをチェックすることを決定した理由、ほとんどのドキュメントが容赦なく再スローすることを提案している理由、およびその例外をスローするときに中断されたフラグをクリアすることを決定した理由、適切なことはほとんどの場合再びtrueに設定することである場合、開いたままになります議論のために。

ただし、.waitが割り込みフラグをチェックする前にロックを解放すると、別のスレッドから小さなドアが開かれ、mExitedブール値が変更されます。残念ながら、wait()メソッドはネイティブであるため、その特定のJVMのソースを検査する必要があります。これは、投稿したコードのコーディングが不十分であるという事実を変えるものではありません。

于 2012-07-24T03:09:56.007 に答える