2012-06-27 3 views
9

Я пришел через код ниже, и мне интересно, если это именно то, что я думаю, что это делает:Обработка InterruptedException при ожидании выхода сигнала (ошибка в Android?)

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

О context: есть еще один поток, который проверяет на mShouldExit (внутри монитора sObject) и выйдет в этом случае.

Это не выглядит правильным для меня. Если произойдет прерывание, он снова установит прерванный статус, поэтому, когда он вернется к sObject.wait(), произойдет другое прерывание Exception и т. Д. И т. Д. Поэтому он никогда не сможет перейти в истинное состояние ожидания (sObject.wait()), т.е. он никогда не выпустит sObject монитор. Это может привести к бесконечному циклу, поскольку другой поток не может установить значение mExiting в true, поскольку он никогда не может войти в монитор sObject. (Поэтому я считаю, что вызов interrupt() является ошибкой, его здесь нельзя использовать.) Я что-то упустил?

Обратите внимание, что фрагмент кода является частью официального исходного кода для Android.

UPDATE: На самом деле ситуация хуже, потому что тот же шаблон используется в Android, когда начинается рендеринг GL. Официальный исходный код 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, иначе рендеринг не запустится).

Если вы будете следовать этим шагам, поток пользовательского интерфейса будет в конечном итоге в бесконечном цикле, так как выше цитируемого код будет держать генерируя InterruptedException (из-за wait()) и, следовательно, поток GL никогда не будет в состоянии установить mWaitingForSurface к ложному.

По моим тестам, кажется, что такой бесконечный цикл также приведет к бесконечной последовательности GC_CONCURRENT сбора мусора (или, по крайней мере, таких сообщений в LogCat). Интересно, кто-то неизвестный плохо определенный вопрос о StackOverflow ранее, которые могут быть связаны: How to solve GC_concurrent freed?

Разве не возможно, что, возможно, его интерфейс тред его прервала флаг установлен верно, и он использовал GLSurfaceView для карта, которую он упоминает? Просто предположение, возможный сценарий.

+0

Возможный дубликат [Обработка InterruptedException в Java] (http://stackoverflow.com/questions/3976344/handling-interruptedexception-in-java), и этот тоже: http://stackoverflow.com/questions/5915156/ how-can-i-kill-a-thread-without-use-stop/5915306 # 5915306 – assylias

+1

Это не имеет никакого отношения к первой теме, которую вы связали (кроме того, что оба связаны с InterruptedException). Это более сложная проблема, и вопрос полностью другой. (Я знаю, что вы написали «возможно», поэтому я просто говорю всем, что это определенно не так). Второй, добавленный после редактирования комментария, более сложен, он точно подтверждает мое подозрение, что вышеуказанный код неверен, но так как это официальный Android-источник, явный ответ от эксперта будет приятным. –

+0

Вы правы - на том факте, что это не дубликат, и на том факте, что сброс прерванного состояния немедленно заставит ждать выхода. У кого-то может быть хорошая причина для этого ... – assylias

ответ

13

Краткая версия: этот код неверен и вызовет бесконечный цикл (у меня все еще есть сомнения, но может зависеть от реализаций JVM). Настройка статуса прерывания - это правильная вещь, но она должна выйти из цикла, в конечном счете, проверить тот же статус прерывания с помощью Thread.isInterrupted().

Длинная версия для случайного читателя:

Проблема состоит в том, чтобы остановить поток, который в настоящее время выполняется некоторая работа, в ответ на кнопку «Отмена» от пользователя или из-за какой-либо другой логики приложения.

Первоначально Java поддерживал метод «stop», который превентивно остановил поток. Этот метод был продемонстрирован как небезопасный, потому что не дал остановленному потоку какой-либо (простой) способ очистки, освободить ресурсы, не подвергать частично модифицированным объектам и т. Д.

Итак, Java превратилась в «совместную» систему «прерывания потока». Эта система довольно проста: поток работает, кто-то называет это «прерывание», флаг установлен в потоке, ответственность за Thread - проверить, была ли она прервана или нет, и действовать соответствующим образом.

Таким образом, правильный Thread.run (или 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 
} 

Это прекрасно, пока весь код ваш поток выполняемым является внутри вашего метода запуска, и вы никогда не называете то, что блокирует в течение длительного времени ... что часто бывает не так, потому что если вы создаете Thread, это потому, что у вас есть что-то долгое.

Простейший метод, который блокирует Thread.sleep (millis), на самом деле это единственное, что он делает: он блокирует поток за заданное количество времени.

Теперь, если прерывание поступает, когда ваш поток находится внутри Thread.sleep (600000000), без какой-либо другой поддержки потребуется много для того, чтобы он добрался до точки, где он проверяет isInterrupted.

Есть ситуации, когда ваша нить никогда не выйдет. Например, ваш поток что-то вычисляет и отправляет результаты в BlockingQueue с ограниченным размером, вы вызываете queue.put (myresult), он будет блокировать до тех пор, пока потребитель не освободит место в очереди, если в то же время потребитель был прервано (или умерло или что-то еще), это пространство никогда не прибудет, метод не вернется, проверка. IsInterrupted никогда не будет выполнена, ваш поток застрял.

Чтобы избежать этой ситуации, все (большинство) методов, которые прерывают поток (должны), бросают InterruptedException. Это исключение просто говорит вам: «Я ждал этого и этого, но в то же время поток как прерванный, вы должны сделать очистку и выйти как можно скорее».

Как и все исключения, если вы не знаете, что делать, вы должны повторно бросить его и надеяться, что кто-то из вас в стеке вызовов знает.

ПрерываниеЭксперименты еще хуже, поскольку, когда они выбрасываются, «прерванный статус» очищается. Это означает, что просто ловить и их игнорирование приведет к потоку, который обычно не останавливается:

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

В этом примере, если приходит прерывание во время метода сна() (который +99,9999999999% времени), он будет вызывать InterruptedException, очистить флаг прерывания, тогда цикл будет продолжаться, так как флаг прерывания является ложным, и поток не останавливается.

Именно поэтому, если вы правильно реализуете свое «пока», используя .isInterrupted, и вам действительно нужно поймать InterruptedException, и у вас нет ничего особенного (например, очистка, возврат и т. Д.), Чтобы сделать с ним меньше что вы можете сделать, это снова установить флаг прерывания.

Проблема в коде, который вы опубликовали, заключается в том, что «while» полагается исключительно на mExited, чтобы решить, когда остановиться, а не ALSO on 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 назад к истине также важно, если вы не контролируете тему.Например, если вы находитесь в runnable, который выполняется в пуле потоков какого-либо типа или внутри любого метода в любом месте, где вы не владеете и не управляете потоком (простой случай: сервлет), вы не знаете, если прерывание для «вас» (в случае с сервлетом клиент закрыл соединение, и контейнер пытается остановить вас, чтобы освободить поток для других запросов) или если он нацелен на поток (или систему) в целом (контейнер закрывается, останавливая все).

В этой ситуации (что составляет 99% от кода), если вы не можете повторить исключение InterruptedException (которое, к сожалению, проверено), единственный способ распространить стек на пул потоков, в котором поток был прерван , перед возвратом вернет флаг true.

Таким образом, он будет распространяться по стеку, в результате генерируя больше InterruptedException, вплоть до владельца потока (будь то сам jvm, Исполнителя или любой другой пул потоков), который может реагировать должным образом (повторно использовать поток, let it die, System.exit (1) ...)

Большая часть этого раскрывается в главе 7 Java Concurrency in Practice, очень хорошей книге, которую я рекомендую всем, кто интересуется компьютерным программированием в целом, а не только Java, потому что проблемы и решения аналогичны во многих других средах, и объяснения очень хорошо написаны.

Почему Sun решила сделать InterruptedException проверенной, когда в большинстве документов предлагается безжалостно реконструировать ее, и почему они решили очистить прерванный флаг, когда выбрасывают это исключение, когда правильная вещь - снова установить его в true время остается открытым для обсуждения.

Однако, если .wait освобождает блокировку ПЕРЕД проверкой флага прерывания, он открывает небольшую дверь из другого потока, чтобы изменить mExited boolean. К сожалению, метод wait() является родным, поэтому источник этой конкретной JVM должен быть проверен. Это не меняет того факта, что код, который вы опубликовали, плохо кодируется.

+1

Кто-то должен опубликовать его как отчет об ошибке в Google, я думаю. Поскольку код, который я опубликовал, является официальным исходным кодом Android. Проверка Grepcode показывает, что он присутствует в новейшем сэндвиче с мороженым. –

+0

Да, это должно быть указано. У меня нет опыта работы с Android, но не стесняйтесь использовать мой ответ в своем отчете об ошибке, если вам нужно. –

+0

Спасибо. У них есть предопределенный формат, которому нужен рабочий пример, чтобы представить ошибку и т. Д., Так что, к сожалению, у меня не будет времени на это в ближайшем будущем. –

Смежные вопросы