четверг, 1 марта 2018 г.

Язык Котлин. Часть 3. Многопоточность. Сопрограммы (корутины).

В Котлин появились удобные и компактные инструменты для организации многопоточности — сопрограммы (или, как калька с английского, корутины). Для их использования в файле build.gradle модуля app в раздел dependencies (зависимости) дописываем строчки:

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:0.22.2"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:0.22.2" // Поддержка UI

Они подключат библиотеки сопрограмм. Далее, проверяем, чтобы вначале этого же файла был подключен плагин обработчика Котлиновских аннотаций:

apply plugin: 'kotlin-kapt' // Обработчик аннотаций для Kotlin (работает и с java-аннотациями).

А разделе dependencies везде annotationProcessor замените на kapt. Это должно быть сделано во всех Котлин-проектах, иначе при компиляции могут вылезать ошибки.

Например, без плагина kotlin-kapt при использовании библиотеки Room при компиляции возникает ошибка в @Dao: Error:Each bind variable in the query must have a matching method parameter. Cannot find method parameters for :id. Часто эту ошибку обходят использованием arg0 вместо id, но это не совсем правильно.

Пример использования сопрограмм

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

override fun onClick(v: View?) {
    Log.i("TEST","1.Нажали кнопку")   // Информируем в лог
    textView.text = "1.Нажали кнопку" // Информируем в интерфейс, тут можно обновлять интерфейс

    // ЗАПУСК «ТЯЖЁЛОЙ РАБОТЫ»
    // Запускаем первую сопрограмму (в фоновом пуле потоков CommonPool) и присваиваем её переменной myThread
    val myThread = async(CommonPool) {                 // Запустить сопрограмму и присвоить её переменной myThread.
        Log.i("TEST","3.Первая сопрограмма запущена")  // Информируем только в лог. Интерфейс обновлять нельзя.
        sleep(2000)                                    // Типа что-то делаем длительное
        Log.i("TEST","6.Первая сопрограмма завершена") // Информируем только в лог. Интерфейс обновлять нельзя.
        "Это типа результат работы первой сопрограммы" // Результат работы сопрограммы - результат последнего действия
    }
    Log.i("TEST","2.Запустили первую сопрограмму и продолжаем")
    // Тут можем продолжать работу, а сопрограмма выполняется в фоне.

    // ПРИЁМ И ОБРАБОТКА РЕЗУЛЬТАТА
    // Запустим вторую сопрограмму (теперь в контексте пользовательского интерфейса)
    // Она просто будет ожидать результат и выведет его в интерфейс
    launch (UI) {  // Запустить и забыть. Владелец сопрограммы - пользовательский интерфейс UI.
        Log.i("TEST","5.Вторая сопрограмма запущена")  // Информируем в лог, но можно и в интерфейс
        val myResult = myThread.await()                // Подождём результата
        Log.i("TEST","7.Вторая сопрограмма завершена, результат: $myResult")   // Информируем в лог, но можно и в интерфейс
        textView.text = "7.Вторая сопрограмма завершена, результат: $myResult" // Выводим результат в интерфейс
    }
    Log.i("TEST","4.Запустили вторую сопрограмму и продолжаем") // Информируем в лог, но можно и в интерфейс
    // Тут можем продолжать работу, а сопрограмма дождётся результата, и сама выведет его в интерфейс.
}

В область импорта добавим библиотеку контекста пользовательского интерфейса:

import kotlinx.coroutines.experimental.android.UI

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

Итак, при нажатии на кнопку мы выводим сообщение об этом в лог и в textView. Затем запускаем асинхронную сопрограмму, и присваиваем её переменной myThread. Присваивается не результат выполнения сопрограммы, а именно живая сопрограмма. Через метод await этой сопрограммы, выраженной переменной, мы сможем в дальнейшем получить и сам результат.

Асинхронная сопрограмма запущена в фоновом пуле потоков CommonPool, и мы не можем напрямую выводить оттуда информацию в поток интерфейса. Поэтому сообщаем о запуске сопрограммы только в лог. Напомню, что эти сообщения мы можем читать во время выполнения программы на вкладке Logcat в нижней части Android Studio.

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

return@async "Это типа результат работы первого потока"

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

После запуска первой асинхронной сопрограммы мы можем продолжить другие операции, а сопрограмма будет в фоне выполнять свою работу. Но как нам узнать, когда она завершится, и как забрать из неё результат? Для этого у нас есть переменная сопрограммы, которой мы эту сопрограмму присвоили ранее. Ожидание завершения сопрограммы и получение результата осуществляется через ожидающую функцию, которой является метод потока myThread.await().

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

Поскольку нам нужно вывести результат в интерфейс, вторую сопрограмму мы запустим не в общем пуле потоков CommonPool, а в контексте пользовательского интерфейса UI. Несмотря на то, что ожидание будет выполняться в потоке пользовательского интерфейса, наличие второй сопрограммы позволит не замораживать его. Ведь мы будем просто ожидать, не выполняя никаких действий.

После того, как первая сопрограмма завершится, вторая сопрограмма сама считает её результат и выведет в интерфейс.

Такая организация распараллеливания задач выглядит гораздо проще, чем работа с дополнительным классом в Java с реализацией его абстрактных методов. Если убрать вывод сообщений в лог, код будет выглядеть ещё компактнее и проще:

override fun onClick(v: View?) {
    val myThread = async(CommonPool) {                 // Запустить сопрограмму и присвоить её переменной myThread.
        sleep(2000)                                    // Типа что-то делаем длительное
        "Это типа результат работы первой сопрограммы" // Результат работы сопрограммы - результат последнего действия
    }
    launch (UI) {                                      // Запустить и забыть. 
        val myResult = myThread.await()                // Подождём результата
        textView.text = "Вторая сопрограмма завершена, результат: $myResult" // Выводим результат в интерфейс
    }
}

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

Помимо сопрограмм acync и launch имеются ещё один вид сопрограммы для системы Андроид. Это сопрограмма runBlocking, позволяющая дождаться выполнение первой фоновой сопрограммы в основном коде с блокированием интерфейса. Используется для написания кода в блокирующем стиле.

Комментариев нет:

Отправить комментарий