Прерывание потока не работает (Java Android)

Изменить: см. здесь!

У меня есть поток с Runnable, показанный ниже. С этим есть проблема, которую я не могу понять: в половине случаев, когда я вызываю interrupt() в потоке (чтобы остановить его), он фактически не завершается (InterruptedException не перехватывается).

private class DataRunnable implements Runnable {
    @Override
    public void run() {
        Log.d(TAG, "DataRunnable started");
        while (true) {
            try {
                final String currentTemperature = HeatingSystem.get("currentTemperature");
                mView.post(() -> showData(currentTemperature));
            } catch (ConnectException e) {
                mView.post(() -> showConnectionMessage());
                break;
            }
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                break;
            }
        }
        Log.d(TAG, "DataRunnable terminated");
    }
}

Проблема заключается в методе HeatingSystem.get(String), который выполняет длительную сетевую операцию. Я предполагаю, что где-то в этом методе флаг прерывания сбрасывается, но я не могу найти, какой оператор будет это делать (я не нашел упоминания об этом в справочниках для всех классов, участвующих в методе, например HttpURLConnection). Способ ниже (написан не мной).

/**
 * Retrieves all data except for weekProgram
 * @param attribute_name
 *            = { "day", "time", "currentTemperature", "dayTemperature",
 *            "nightTemperature", "weekProgramState" }; Note that
 *            "weekProgram" has not been included, because it has a more
 *            complex value than a single value. Therefore the funciton
 *            getWeekProgram() is implemented which return a WeekProgram
 *            object that can be easily altered.
 */
public static String get(String attribute_name) throws ConnectException,
        IllegalArgumentException {
    // If XML File does not contain the specified attribute, than
    // throw NotFound or NotFoundArgumentException
    // You can retrieve every attribute with a single value. But for the
    // WeekProgram you need to call getWeekProgram().
    String link = "";
    boolean match = false;
    String[] valid_names = {"day", "time", "currentTemperature",
            "dayTemperature", "nightTemperature", "weekProgramState"};
    String[] tag_names = {"current_day", "time", "current_temperature",
            "day_temperature", "night_temperature", "week_program_state"};
    int i;
    for (i = 0; i < valid_names.length; i++) {
        if (attribute_name.equalsIgnoreCase(valid_names[i])) {
            match = true;
            link = HeatingSystem.BASE_ADDRESS + "/" + valid_names[i];
            break;
        }
    }

    if (match) {
        InputStream in = null;
        try {
            HttpURLConnection connect = getHttpConnection(link, "GET");
            in = connect.getInputStream();

            /**
             * For Debugging Note that when the input stream is already used
             * with this BufferedReader, then after that the XmlPullParser
             * can no longer use it. This will cause an error/exception.
             *
             * BufferedReader inn = new BufferedReader(new
             * InputStreamReader(in)); String testLine = ""; while((testLine
             * = inn.readLine()) != null) { System.out.println("Line: " +
             * testLine); }
             */
            // Set up an XML parser.
            XmlPullParser parser = Xml.newPullParser();
            parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES,
                    false);
            parser.setInput(in, "UTF-8"); // Enter the stream.
            parser.nextTag();
            parser.require(XmlPullParser.START_TAG, null, tag_names[i]);

            int eventType = parser.getEventType();

            // Find the single value.
            String value = "";
            while (eventType != XmlPullParser.END_DOCUMENT) {
                if (eventType == XmlPullParser.TEXT) {
                    value = parser.getText();
                    break;
                }
                eventType = parser.next();
            }

            return value;
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (FileNotFoundException e) {
            System.out.println("FileNotFound Exception! " + e.getMessage());
            // e.printStackTrace();
        } catch (XmlPullParserException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (in != null)
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
        }
    } else {
        // return null;
        throw new IllegalArgumentException("Invalid Input Argument: \""
                + attribute_name + "\".");
    }
    return null;
}

/**
 * Method for GET and PUT requests
 * @param link
 * @param type
 * @return
 * @throws IOException
 * @throws MalformedURLException
 * @throws UnknownHostException
 * @throws FileNotFoundException
 */
private static HttpURLConnection getHttpConnection(String link, String type)
        throws IOException, MalformedURLException, UnknownHostException,
        FileNotFoundException {
    URL url = new URL(link);
    HttpURLConnection connect = (HttpURLConnection) url.openConnection();
    connect.setReadTimeout(HeatingSystem.TIME_OUT);
    connect.setConnectTimeout(HeatingSystem.TIME_OUT);
    connect.setRequestProperty("Content-Type", "application/xml");
    connect.setRequestMethod(type);
    if (type.equalsIgnoreCase("GET")) {
        connect.setDoInput(true);
        connect.setDoOutput(false);
    } else if (type.equalsIgnoreCase("PUT")) {
        connect.setDoInput(false);
        connect.setDoOutput(true);
    }
    connect.connect();
    return connect;
}

Кто-нибудь знает, что в методе выше может вызвать проблему?

Thread.sleep() также выдает InterruptException, когда interrupt() вызывается перед вводом Thread.sleep(): () с установленным *статусом прерывания*? .

Я проверил, достигается ли Thread.sleep() после прерывания, и он достигается.

Вот как DataRunnable запускается и прерывается (я всегда получаю журнал "onPause call"):

@Override
public void onResume() {
    connect();
    super.onResume();
}

@Override
public void onPause() {
    Log.d(TAG, "onPause called");
    mDataThread.interrupt();
    super.onPause();
}

private void connect() {
    if (mDataThread != null && mDataThread.isAlive()) {
        Log.e(TAG, "mDataThread is alive while it shouldn't!"); // TODO: remove this for production.
    }
    setVisibleView(mLoading);
    mDataThread = new Thread(new DataRunnable());
    mDataThread.start();
}

person mhvis    schedule 17.06.2015    source источник
comment
InterruptedException выдается только тогда, когда Thread.sleep выполняется, пока вы вызываете для него Thread.interrupt(). Вам также потребуется опросить статус потока, чтобы проверить, не прерван ли он. И если это так, прекратить дальнейшую работу.   -  person Measuring    schedule 17.06.2015
comment
@Measuring Я разместил ссылку в конце вопроса, в которой говорится, что она также должна работать, когда Thread.interrupt() вызывается перед входом в Thread.sleep(). Во всяком случае, я попробовал это, выполнив if (Thread.currentThread().isInterrupted()) break; перед Thread.sleep(), но проблема не устранена.   -  person mhvis    schedule 17.06.2015
comment
Можете ли вы включить код, в котором вы создаете DataRunnable, а также ту часть, где вы его прерываете?   -  person Measuring    schedule 17.06.2015
comment
@Measuring Включены детали, спасибо за отзыв.   -  person mhvis    schedule 17.06.2015
comment
Мог ли connect вызываться несколько раз? Создание нескольких потоков с одним и тем же Runnable? Я также ошибался в том, что Thread.sleep() не выдает InterruptedException, если поток уже прерван. Простите за это. Я также не думаю, что смогу увидеть проблему без отладки кода. Возможно, стоит попытаться добавить System.out.println в код Runnable, чтобы получить хорошее представление о том, что занимает много времени и что выполняется.   -  person Measuring    schedule 18.06.2015
comment
@Измерение не имеет значения, что вы это пропустили. Я собираюсь сделать еще немного отладки. Уже многое перепробовал (путем отключения кода и логирования) и вроде бы приводит к проблеме в методе HeatingSystem.get(String). Однако я не полностью отлаживал этот метод, сделаю это. Кстати, connect() вызывается только один раз.   -  person mhvis    schedule 18.06.2015
comment
Вот как я бы отладил это. Я бы прервал сам поток, добавив вызов Thread.currentThread().interrupt(); в самом начале метода DataRunnable run(). Затем я добавлял кучу System.out.println("line XX - " + Thread.currentThread().isInterrupted()); утверждений повсюду. Таким образом, я сужу линию, в которой состояние прерывания внезапно меняется на ложное. Надеюсь это поможет. (или вместо печати просто отлаживайте каждую строку, наблюдая за значением isInterrupted())   -  person sstan    schedule 18.06.2015
comment
@sstan Я не думал вызывать прерывание в начале метода run(), но кажется, что подсказка ведет к причине. Я получаю трассировку стека прямо сейчас с java.io.InterruptedIOException в строке in = connect.getInputStream();. Я еще не знаю, почему я не видел этого раньше, вероятно, потому, что вызовы прерывания в случайные моменты в основном относятся к самому длинному оператору, в который момент, по-видимому, printStackTrace() не вызывается. Я напишу больше позже.   -  person mhvis    schedule 18.06.2015


Ответы (3)


Я боролся с подобными проблемами в прошлом, и я никогда не находил удовлетворительного решения этой проблемы. Единственным обходным решением, которое всегда работало для меня, было введение изменчивой логической переменной-члена «stopRequested», для которой установлено значение true, чтобы прервать Runnable и проверить условие while Runnable вместо состояния прерывания потока.

Неприятная деталь моего опыта с этим заключается в том, что мне никогда не удавалось извлечь небольшой компилируемый пример, который воспроизводит эту проблему. Вы в состоянии сделать это? Если вы не можете этого сделать, мне было бы интересно узнать, уверены ли вы, что работаете с правильным экземпляром mDataThread? Вы можете проверить это, записав его хэш-код...

person edr    schedule 17.06.2015
comment
Из любопытства, вы сталкивались с этим и на Android, как OP? - person sstan; 18.06.2015
comment
@sstan Нет, я сталкивался с этим в стандартной Java и в основном при работе с исполнителями и фьючерсами. - person edr; 18.06.2015
comment
Принято, так как я использую обходной путь, также см. это - person mhvis; 19.06.2015

Это не совсем ответ, но я не знаю, куда его поставить. При дальнейшей отладке я столкнулся со странным поведением и смог воспроизвести его в тестовом классе. Теперь мне любопытно, действительно ли это неправильное поведение, как я подозреваю, и могут ли другие люди воспроизвести его. Я надеюсь, что это сработает, чтобы написать это как ответ.

(Может быть, лучше сделать это новым вопросом или просто отправить отчет об ошибке?)

Ниже приведен тестовый класс, его нужно запускать на Android, так как речь идет о вызове getInputStream(), который ведет себя по-разному на Android (точно не знаю, почему). На Android getInputStream() при прерывании выдает InterruptedIOException. Поток ниже зацикливается и прерывается через секунду. Таким образом, когда он прерывается, исключение должно быть сгенерировано getInputStream() и должно быть перехвачено с помощью блока catch. Иногда это работает правильно, но в большинстве случаев исключение не выдается! Вместо этого сбрасывается только флаг прерывания, который изменяется с interrupted==true на interrupted==false, что затем перехватывается if. У меня всплывает сообщение в if. Мне кажется это неправильным поведением.

import java.net.HttpURLConnection;
import java.net.URL;

class InterruptTest {

InterruptTest() {
    Thread thread = new Thread(new ConnectionRunnable());
    thread.start();
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {}
    thread.interrupt();
}

private class ConnectionRunnable implements Runnable {
    @Override
    public void run() {
        while (true) {
            try {
                URL url = new URL("http://www.google.com");
                HttpURLConnection connect = (HttpURLConnection) url.openConnection();

                boolean wasInterruptedBefore = Thread.currentThread().isInterrupted();
                connect.getInputStream(); // This call seems to behave odd(ly?)
                boolean wasInterruptedAfter = Thread.currentThread().isInterrupted();

                if (wasInterruptedBefore == true && wasInterruptedAfter == false) {
                    System.out.println("Wut! Interrupted changed from true to false while no InterruptedIOException or InterruptedException was thrown");
                    break;
                }
            } catch (Exception e) {
                System.out.println(e.getClass().getName() + ": " + e.getMessage());
                break;
            }
            for (int i = 0; i < 100000; i += 1) { // Crunching
                System.out.print("");
            }
        }
        System.out.println("ConnectionThread is stopped");
    }
}
}
person mhvis    schedule 18.06.2015
comment
ну, может быть, это не ответ, но я рад, что вы его опубликовали. Мне было очень любопытно, что вы найдете. Надеюсь, вы отправите отчет об ошибке. И что касается того, чтобы заставить это работать на вас, я думаю, что предложение edr использовать ваш собственный флаг - правильный путь. Удачи! - person sstan; 19.06.2015

Вам нужно будет изменить цикл while на:

// You'll need to change your while loop check to this for it to reliably stop when interrupting.
while(!Thread.currentThread().isInterrupted()) {
    try {
        final String currentTemperature = HeatingSystem.get("currentTemperature");
        mView.post(() -> showData(currentTemperature));
    } catch (ConnectException e) {
        mView.post(() -> showConnectionMessage());
        break;
    }

    try {
        Thread.sleep(10);
    } catch (InterruptedException e) {
        break;
    }
}

Вместо while(true).

Обратите внимание, что Thread.interrupted() и Thread.currentThread().isInterrupted() делают разные вещи. Первый сбрасывает прерванный статус после проверки. Последний оставляет статус без изменений.

person Measuring    schedule 17.06.2015
comment
Я пробовал это, но он по-прежнему завершается только в половине случаев при вызове interrupt() в потоке. Эта проверка уже сделана в конце цикла while. - person mhvis; 17.06.2015