Поведение циклов блокировки пользовательского интерфейса отличается (Oreo против Mashmallow) - PullRequest
0 голосов
/ 26 ноября 2018

У меня есть небольшое приложение для Android, которое выполняет вызов сервера для публикации некоторых пользовательских данных на сервере.Ниже приведен код:

private boolean completed = false;
public String postData( Data data){

    new Thread(new Runnable() {
        @Override
        public void run() {

            try{

                String response = callApi(data);

                completed = true;



            }catch(Exception e){

                Log.e("API Error",e.getMessage());
                completed = true;
                return;
            }


        }
    }).start();

    while(!completed){

  //      Log.i("Inside loop","yes");
    }

    return response.toString();
}

Вышеуказанный метод вызывает API для публикации данных и возвращает полученный ответ, который работает нормально.Цикл внизу - это цикл блокировки пользовательского интерфейса, который блокирует пользовательский интерфейс до получения ответа или ошибки.

Проблема:

Я пробовал один и тот же код для устройства Marshmallow и Oreo ирезультаты были разные.

Для Зефира: Дела пошли вразрез с моими ожиданиями.:)

Для Oreo (8.1.0):

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

Однако при установке точек останова (работающих в режиме отладки)Приложение движется с гораздо меньшими проблемами.

Кажется, что система не может выйти из цикла блокировки пользовательского интерфейса, хотя условие выполнено.

Второе поведение, которое было замечено, - это когда я регистрирую сообщение вВ потоке блокировки пользовательского интерфейса система может выйти из цикла и вернуться из метода, хотя ответ API не регистрируется.

Может ли кто-нибудь помочь понять такое несоответствие между этими двумя разновидностями Android и что может быть изменениемВведено ли такое поведение для Орео, но не для Зефира?Любое понимание было бы чрезвычайно полезно.

1 Ответ

0 голосов
/ 27 ноября 2018

Скорее всего, будут различия в реализации кэша процессора на двух разных аппаратных устройствах, которые вы используете.Наверное, совсем не JVM.

Согласованность памяти - довольно сложная тема, я рекомендую проверить учебник, подобный this , для более углубленного изучения.Также см. Этот объяснитель модели памяти Java для получения подробной информации о гарантиях, которые JVM предоставит, независимо от вашего оборудования.

Я объясню гипотетический сценарий, в которомповедение, которое вы наблюдали, могло произойти, не зная конкретных деталей вашего чипсета:

ГИПОТЕТИЧЕСКИЙ СЦЕНАРИЙ

Два потока: Ваш «поток пользовательского интерфейса» (скажем, этоработает на ядре 1) и "фоновом потоке" (ядро 2).Вашей переменной completed назначается одна фиксированная ячейка памяти во время компиляции (предположим, что мы разыменовали this и т. Д., И мы установили, что это за местоположение).completed представлен одним байтом, начальное значение «0».

Поток пользовательского интерфейса на ядре 1 быстро достигает цикла занятости-ожидания.В первый раз, когда он пытается прочитать completed, возникает «ошибка кэша».Таким образом, запрос отправляется через в кэш и читает completed (вместе с другими 31 байтами в строке ) из основной памяти.Теперь, когда строка кэша находится в кеше L1 ядра 1, она считывает значение и обнаруживает, что это «0».(Ядра не подключены напрямую к основной памяти; они могут получить к ней доступ только через кеш.) Итак, ожидание занятости продолжается;ядро 1 запрашивает одно и то же место в памяти, completed, снова и снова, но вместо пропуска кэша L1 теперь может удовлетворить каждый запрос и больше не нуждается в связи с основной памятью.

Между тем, вклВ ядре 2 фоновый поток работает для завершения вызова API.В конце концов он завершает работу и пытается записать «1» в ту же ячейку памяти, completed.Опять же, происходит сбой кэша, и происходит то же самое.Ядро 2 записывает «1» в соответствующее место в своем собственном кэше L1.Но эта строка кэша еще не обязательно записывается обратно в основную память.Даже если бы это было так, ядро ​​1 все равно не ссылается на основную память, поэтому оно не увидит изменений.Затем ядро ​​2 завершает поток, возвращается и уходит, чтобы выполнить работу где-то еще.

(К тому времени, когда ядро ​​2 назначено другому процессу, его кэш, вероятно, синхронизирован с основной памятью и очищен.Итак, «1» возвращается в основную память. Не то, чтобы это имело какое-либо значение для ядра 1, которое продолжает работать исключительно из своего кэша L1.)

И все продолжается таким образом, пока что-то не произойдетслучается, чтобы предложить кэшу ядра 1, что он грязный, и он должен обновить.Как я упоминал в комментариях, это может быть fence , возникающий как часть вызова System.out.println(), записи отладчика и т. Д. Естественно, если бы вы использовали блок synchronized, компиляторпоместили забор в ваш собственный код.

TAKEAWAYS

... и поэтому вы всегда защищаете доступ к общим переменным с помощью блока synchronized!(Таким образом, вам не нужно тратить дни на чтение руководств процессора, пытаясь понять детали модели памяти на конкретном оборудовании, которое вы используете, просто для того, чтобы разделить байт информации между двумя потоками.) Ключевое слово volatile также будетрешить проблему, но см. некоторые ссылки в статье Jenkov для сценариев, в которых этого недостаточно.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...