Чем сопрограммы Kotlin отличаются от Executor Java в Android? - PullRequest
3 голосов
/ 11 апреля 2019

Я разработчик Android и перехожу с Java на Kotlin, и я планирую использовать сопрограммы для обработки асинхронного кода, поскольку он выглядит очень многообещающе.

Вернемся в Java для обработки асинхронного кода, который я использовалкласс Executor для выполнения трудоемкого фрагмента кода в другом потоке, вне потока пользовательского интерфейса.У меня был AppExecutors класс, который я вводил в мои xxxRepository классы для управления набором Executor.Выглядело это так:

public class AppExecutors
{
    private static class DiskIOThreadExecutor implements Executor
    {
        private final Executor mDiskIO;

        public DiskIOThreadExecutor()
        {
            mDiskIO = Executors.newSingleThreadExecutor();
        }

        @Override
        public void execute(@NonNull Runnable command)
        {
            mDiskIO.execute(command);
        }
    }

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

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

    private static volatile AppExecutors INSTANCE;

    private final DiskIOThreadExecutor diskIo;
    private final MainThreadExecutor mainThread;

    private AppExecutors()
    {
        diskIo = new DiskIOThreadExecutor();
        mainThread = new MainThreadExecutor();
    }

    public static AppExecutors getInstance()
    {
        if(INSTANCE == null)
        {
            synchronized(AppExecutors.class)
            {
                if(INSTANCE == null)
                {
                    INSTANCE = new AppExecutors();
                }
            }
        }
        return INSTANCE;
    }

    public Executor diskIo()
    {
        return diskIo;
    }

    public Executor mainThread()
    {
        return mainThread;
    }
}

Тогда я смог написать такой код в моем xxxRepository:

executors.diskIo().execute(() ->
        {
            try
            {
                LicensedUserOutput license = gson.fromJson(Prefs.getString(Constants.SHAREDPREF_LICENSEINFOS, ""), LicensedUserOutput.class);

                /**
                 * gson.fromJson("") returns null instead of throwing an exception as reported here :
                 * https://github.com/google/gson/issues/457
                 */
                if(license != null)
                {
                    executors.mainThread().execute(() -> callback.onUserLicenseLoaded(license));
                }
                else
                {
                    executors.mainThread().execute(() -> callback.onError());
                }
            }
            catch(JsonSyntaxException e)
            {
                e.printStackTrace();

                executors.mainThread().execute(() -> callback.onError());
            }
        });

Он работал очень хорошо, и у Google даже есть что-то похожее вих много примеров Github для Android репо.

Так что я использовал обратные вызовы.Но теперь я устал от вложенных обратных вызовов и хочу от них избавиться.Для этого я мог бы написать в своем xxxViewModel, например:

executors.diskIo().execute(() -> 
        {
            int result1 = repo.fetch();
            String result2 = repo2.fetch(result1);

            executors.mainThread().execute(() -> myLiveData.setValue(result2));
        });

Чем это ИСПОЛЬЗОВАНИЕ отличается от использования сопрограмм Котлина?Из того, что я увидел, их самое большое преимущество - возможность использовать асинхронный код в стиле последовательного кода.Но я могу сделать это только с Executor, как вы можете видеть из примера кода выше.Так чего мне здесь не хватает?Что бы я получил, чтобы перейти с Executor на сопрограммы?

Ответы [ 2 ]

2 голосов
/ 12 апреля 2019

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

Я также признаюсь, что я использовал куротины и актеры Котлина только в течение 6 месяцев., но давайте продолжим.

Async IO

Итак, я думаю, что одно большое отличие состоит в том, что выполнение вашей задачи в сопрограмме позволит вам достичь параллелизма наодин поток для задачи ввода-вывода, если эта задача является действительно асинхронной задачей ввода-вывода, которая должным образом возвращает управление, пока задача ввода-вывода еще выполняется.Таким образом вы можете добиться очень легкого одновременного чтения / записи с сопрограммами.Вы можете запустить 10 000 сопрограмм, одновременно считывающих данные с диска в одном потоке, и это будет происходить одновременно.Вы можете прочитать больше об асинхронном вводе-выводе здесь async io wiki

Для службы Executor, с другой стороны, если у вас есть 1 поток в вашем пуле, ваши несколько задач ввода-вывода будут выполняться и блокироваться всерия на этой теме.Даже если вы использовали асинхронную библиотеку.

Структурированный параллелизм

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

Взаимодействие с актерами

Другое, вероятно, более нишевое преимущество заключается в том, что с сопрограммами, производителями и потребителями вы можете взаимодействовать с актерами.Действующие лица инкапсулируют состояние и обеспечивают параллельный параллельный поток благодаря общению, а не традиционным синхронизированным инструментам.Используя все это, вы можете достичь очень легкого и высококонкурентного состояния с минимальными накладными расходами.Исполнители просто не предлагают возможность взаимодействовать с синхронизированным состоянием в чем-то вроде актера, например, с 10 000 потоков или даже 1000 потоков.Вы можете с радостью запустить 100 000 сопрограмм, и если задачи приостанавливаются и дают контроль в подходящих точках, вы можете добиться некоторых превосходных результатов.Вы можете прочитать больше здесь Общее изменяемое состояние

Легкий вес

И, наконец, просто чтобы продемонстрировать, насколько легок параллельный параллелизм сопрограмм, я бы бросил вызоввам нужно сделать что-то подобное на исполнителе и посмотреть, каково общее прошедшее время (это завершено за 1160 миллисекунд на моей машине):

fun main() = runBlocking {
    val start = System.currentTimeMillis()
    val jobs = List(10_000){
        launch {
            delay(1000) // delays for 1000 millis
            print(".")
        }
    }
    jobs.forEach { it.join() }
    val end = System.currentTimeMillis()
    println()
    println(end-start)
}

Возможно, есть и другие вещи, но, как я уже сказал, явсе еще учусь.

0 голосов
/ 15 апреля 2019

Хорошо, я нашел ответ сам, когда использовал сопрограммы в своих приложениях.Для напоминания я искал разницу в использовании .Мне удалось последовательно выполнить асинхронный код с Executor, и я везде видел, что это было наибольшим преимуществом сопрограмм, так в чем же преимущество перехода на сопрограммы?

Во-первых, вы можете видеть из моего последнего примера, что именно xxxViewModel выбирает, в каком потоке выполняются асинхронные задачи.Это на мой взгляд недостаток дизайна.ViewModel не должна знать об этом и тем более нести ответственность за выбор потока.

Теперь с сопрограммами я могу написать что-то вроде этого:

// ViewModel
viewModelScope.launch {
    repository.insert(Title(title = "Hola", id = 1))
    myLiveData.value = "coroutines are great"
}
// Repository
suspend fun insert(title: Title)
{
    withContext(Dispatchers.IO)
    {
        dao.insertTitle(title)
    }
}

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

Кроме того, отмена сопрограмм намного проще, чем ExecutorService отмена.ExecutorService на самом деле не предназначен для отмены.У него есть метод shutdown(), но он отменяет все задачи ExecutorService, а не только те, которые нам нужно отменить.Если область действия ExecutorService больше, чем у нашей модели, мы облажались.С сопрограммами это так просто, что вам даже не нужно заботиться об этом.Если вы используете viewModelScope (вы должны), он отменит все сопрограммы в этой области в методе onCleared() viewmodel, сам по себе.

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

...