Параллелизм считается одной из самых сложных задач среди всех областей вычислений. Я впервые познакомился с параллельным программированием на языках высокого уровня, и магия не исчезла, пока я не изучил ее на C / C ++.

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

Заставить себя изучить эту тему должно быть легко: Медленная программа создает неудобства для пользователя, а если программа слишком медленная, ОС Android предложит пользователю убить ее огнем .

Вы все еще можете изучить основы параллелизма из этой статьи, если вы разработчик Kotlin Android, но в Kotlin я использую просто сопрограммы; на случай, если вам интересно.

В этой практической статье (с примерами кода) я объясню:

  • Что такое параллелизм
  • Как создать класс, который использует одну из стандартных библиотек Java: Executors (java.util.concurrent.Executors)
  • Как подключить класс Executor к ОС Android для доступа к основному потоку Android (он же поток пользовательского интерфейса)
  • Как на самом деле использовать его для написания параллельного кода

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

Если вы предпочитаете узнать об этом в коротком (‹4-х минутном) видео, зацените .

Параллелизм

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

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

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

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

Дополнительное примечание: возможно выполнение межпроцессного взаимодействия (IPC), но по очевидным причинам ОС Android очень тщательно управляет IPC; в то время как многопоточность в пространстве памяти процесса гораздо менее ограничительна.

Не беспокойтесь слишком сильно, если это кажется расплывчатым; Я изо всех сил старался объяснить это просто, но это означает, что я опускаю детали реализации. Если вы понимаете общую идею, вы сможете использовать потоки на практике после того, как я покажу вам, как это сделать.

Что такое исполнитель?

public interface Executor {
    public void execute(Runnable runnable);
}

На самом деле это просто интерфейс Java с единственным вызовом абстрактного метода execute, который принимает Runnable в качестве аргумента.

public interface Runnable {
    public void run();
}

Если вы не знаете, как работают интерфейсы и абстрактные классы, то я предлагаю пройти мой вводный курс по Java, который объясняет это без использования множества жаргонных слов.

В любом случае, все, что вам нужно понять, это то, что Executor - это в принципе способ, которым мы можем передать исполняемый код в один из потоков в процессе Android. Чтобы, надеюсь, прояснить некоторую путаницу, подумайте об этом так:

  • Исполнитель и его метод execute (…) - это лицо / механизм доставки.
  • Runnable - это доставляемый пакет, а его метод run (…) - это то, как получатель пакета распаковывает и использует код.

Если вы все еще не уверены, дайте ему время и не позволяйте, чтобы это помешало вам изучить параллелизм на практике.

Настройка ваших исполнителей

На самом деле это сделать очень просто. Прежде чем продолжить, я хотел бы поблагодарить Флорину Мунтенеску и Хосе Альсеррека (и всех, кто участвует в Схемах архитектуры Android). Мне посчастливилось натолкнуться на их простую и эффективную реализацию, на которой основан приведенный ниже код.

ApplicationExecutors.java:

public class ApplicationExecutors {

    private final Executor background;
    private final Executor mainThread;

    public Executor getBackground() {
        return background;
    }

    public Executor getMainThread() {
        return mainThread;
    }

    public ApplicationExecutors() {
        this.background = Executors.newSingleThreadExecutor();
        this.mainThread = new MainThreadExecutor();
    }

    private static class MainThreadExecutor implements Executor {
        private Handler mainThreadHandler = new Handler(
                       Looper.getMainLooper()
        );

        @Override
        public void execute(Runnable command) {
            mainThreadHandler.post(command);
        }
    }
}

Есть много способов создать Executor, но здесь мы просто читаем или записываем некоторые данные в локальное хранилище устройства. Следовательно, будет работать один поток за нашей ссылочной переменной Executor вместо набора потоков, таких как Thread Pool.

Здесь я использую статический фабричный метод из Executors.java (не путать с Executor.java) для создания нового потока, обернутого Executor:

this.background = Executors.newSingleThreadExecutor();

Даже самые молодые разработчики знают, что у каждого процесса Android есть специальный поток, который он использует для рисования, мониторинга и обновления пользовательского интерфейса (например, «Действия» и «Фрагменты»). Поскольку это что-то специфическое для ОС Android (т. Е. не является частью стандартной библиотеки Java), нам нужно найти способ привязать этот специальный поток , который обычно называется mainThread или uiThread:

private static class MainThreadExecutor implements Executor {
    private Handler mainThreadHandler = new Handler(Looper.getMainLooper());

    @Override
    public void execute(Runnable command) {
        mainThreadHandler.post(command);
    }
}

Для тех, кто не знаком с библиотеками платформы Android, такими как Handler и Looper, просто поймите, что MainThreadExecutor оборачивается вокруг Handler, а Handler.post (…) - это то же самое, что и Executor.execute (…), но с другим именем. Это позволяет нам безопасно загружать исполняемый код в mainThread. Причина, по которой это отличается от простого создания newSingleThreadExecutor (), заключается в том, что mainThread поступает из системы, а не из того, что мы создаем.

Использование ваших исполнителей

После настройки этого небольшого класса мы можем теперь его использовать. На данный момент игнорируйте Продолжение ‹Day› и просто предположите, что это любой метод Java, который собирается выполнить вызов какого-либо локального устройства хранения (файловая система, комната, область, что угодно). Если вы не можете читать лямбда-выражения, я покажу вам, как они выглядят без:

@Override
public void getDay(Continuation<Day> continuation) {
    //before this line, we are on the mainThread by default
    exec.getBackground().execute(
            () -> {
                //In this block, we are now on a background thread
                Object data;
                try {
                    data = getDayFromStorage();
                } catch (Exception e) {
                    data = e;
                    Log.d("STORAGE", Log.getStackTraceString(e));
                }

                Log.d("CURRENT_THREAD", Long.toString(Thread.currentThread().getId()));

                final Object finalData = data;

                exec.getMainThread().execute(
                        () -> {
                         //now we jump back onto the mainThread
                            Log.d("CURRENT_THREAD", Long.toString(Thread.currentThread().getId()));

                            if (finalData instanceof Day) continuation.onSuccess(
                                    (Day) finalData
                            );

                            else continuation.onException(
                                    (Exception) finalData
                            );
                        }
                );
            }
    );
}

Вот что происходит в ультра-профессиональной и высокобюджетной инфографике:

О лямбдах: мы знаем, что когда мы вызываем Exector.execute (Runnable runnable), нам лучше дать ему Runnable. В данном случае лямбда - это просто способ написать анонимный класс без использования старой школы синдрома запястного канала, вызывающего (позаимствовав фразу из Дугласа С. Шмидта) синтаксис Java:

exec.getBackground().execute(new Runnable() {
    @Override
    public void run() {
        //this is where we do background stuff
        Object data;
        try {
            data = getDayFromStorage();
        } catch (Exception e) {
            data = e;
            Log.d("STORAGE", Log.getStackTraceString(e));
        }
        //...
        exec.getMainThread().execute(
                     new Runnable() {
                         @Override
                         public void run() {
                             //...
                         }
                    }
        );
    }
});

Примерно в третий раз я напоминаю всем, кто смущен, что Runnable просто представляет собой некоторый кусок кода, который мы хотим выполнить, который в данном случае является телом метода run нашего лямбда / анонимного класса.

Что такое продолжение?

Просто удобный способ моделировать успешные и неудачные операции в виде интерфейса…:

public interface Continuation<T> {
    public void onSuccess(T result);

    public void onException(Exception e);
}

… Который предоставляется тем классом, который вызывает наш класс хранилища:

/Decision maker (logic) class for the Front End
public class DayViewLogic extends BaseViewLogic<DayViewEvent> {

    private final IDayViewContract.View view;
    private final IDayViewContract.ViewModel vm;

    //backend IO devices
    private final IDayStorage dayStorage;
    private final ITaskStorage taskStorage;

    public DayViewLogic(IDayViewContract.View view, IDayViewContract.ViewModel vm, IDayStorage dayStorage, ITaskStorage taskStorage) {
        this.view = view;
        this.vm = vm;
        this.dayStorage = dayStorage;
        this.taskStorage = taskStorage;
    }

//...
    private void onStart() {
        dayStorage.getDay(new Continuation<Day>() {
            @Override
            public void onSuccess(Day result) {
                getTasks(result);
            }

            @Override
            public void onException(Exception e) {
                view.showMessage(Messages.GENERIC_ERROR_MESSAGE);
                view.restartFeature();
            }
        });
    }

//...
}

Мы не можем использовать здесь лямбда-выражение, потому что лямбда-выражения в Java (по крайней мере, используемая мной версия может иметь только один метод, например Runnable.run (…) или Executor.execute (…).

Удачного кодирования!

Социальные сети | Служба поддержки

Эту статью написал Райан Майкл Кей. Я программист / инженер-самоучка, создающий образовательный контент по широкому кругу тем на самых разных платформах. Лучший способ поддержать меня - подписаться на меня на различных платформах и присоединиться к моему сообществу разработчиков (у нас сотни участников!):

Объявления:

Https://www.instagram.com/rkay301/
https://www.facebook.com/wiseassblog
https://twitter.com/wiseAss301

Руководства и курсы:

Бесплатные уроки, вопросы и ответы в реальном времени, программирование в реальном времени:
https://www.youtube.com/channel/UCSwuCetC3YlO1Y7bqVW5GHg

Программирование рабочего стола Java с JavaFX (средний уровень) - https://skl.sh/31pzCa1

Полное введение в программирование на Java для новичков (начинающий - средний) - https://skl.sh/3fZbjos

Приложения для Android с Kotlin и Android Studio (для начинающих) - https://skl.sh/2ZU6ZT9

Разработка материалов для Android с использованием Kotlin (средний уровень) - https://skl.sh/2OrwrYZ

Подключиться:

LinkedIn- https://www.linkedin.com/in/ryan-kay-808388114/