Kotlin Coroutines #1

Введение

Coroutines (сопрограммы) — это шаблон проектирования параллелизма, который позволяет выполнять асинхронный код элегантным способом. кроме того, он очень легкий, поэтому Google решил поощрять разработчиков Android использовать его. Помните, что смартфоны — это не серверы, каждая оптимизация важна!

Плюсы сопрограмм

Итак, почему вы должны перейти на сопрограмму в своем приложении:

  • Корутины избавят вас от обратных вызовов.
  • Корутины имеют простой способ переключения и связи между различными потоками
  • Дружественный к Kotlin синтаксис
  • Обработчик исключений прост в реализации
  • Работа с потоками данных
  • Корутины могут воздействовать на несколько асинхронных входов

Когда вы переходите на Coroutines, это огромный плюс. 
Если вы работаете с RxJava, то перейдя на Coroutines можете также использовать потоки для потоковой передачи данных.

Итак, теперь давайте отправимся в путешествие по изучению сопрограмм.

Добавим зависимости

Для Intelliji IDEA, создайте новый проект и добавить основную зависимость сопрограмм в файл build.gradle:

dependencies {
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0'
}

Для Android Studio, создайте новый проект и добавить основную зависимость сопрограмм в файл build.gradle (Module):

dependencies {
    def coroutinesVersion = '1.5.0'
    api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion"
    api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
}

CoroutineScope

После этого создайте новый класс, вы можете назвать его HelloWorld, если хотите. внутри этого класса мы создадим функцию, назовем ее main(), которая будет печатать «Hello World!» внутри CoroutineScope. Мы обсудим, что такое Scope и что такое Context в сопрограммах. Итак, код будет таким:

fun main() {
    GlobalScope.launch {
        delay(1000)
        println("World!")
    }

    print("Hello ")
    Thread.sleep(2000) //Задержка ожидания сопрограммы
}

/* 
Вывод на консоль:
Hello World!
*/

Обратите внимание, что у нас есть маленькая иконка слева от метода delay(). Это подсказка от IDEA, что эта функция является функцией Coroutine.

Теперь давайте разберемся, что произошло

Сначала программа создаст сопрограмму, которая задержится на одну секунду, а затем напечатает строку «Мир!». Эта сопрограмма будет выполняться в глобальном потоке, когда мы запустили ее в GlobalScope!

Затем у нас есть print(“Hello”), который напечатает строку “Hello” в консоли, этот код выполняется в основном потоке, и мы не добавили задержку, поэтому это будет написано на первом месте, а через одну секунду строка «Мир!», поэтому «Мир» печатается после «Hello».

Поздравляем, вы написали свою первую программу Coroutine

Scope

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

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

Существует также вариант, runBlocking , который позволяет приложению ждать этой сопрограммы, пока она не завершится, а затем завершить приложение, по этой причине это называется «блокировкой». Чтобы запустить блокирующую сопрограмму, просто сделайте следующее:

runBlocking { 
    // Ваш код
}

Таким образом, программа будет приостановлена ​​до тех пор, пока эта сопрограмма не завершит свое выполнение.

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

fun main() { 
    println ("Выполнение программы будет заблокировано") 
    runBlocking { 
        launch {
             delay(1000) 
            println ("задача из runBlocking") 
        }

         GlobalScope. launch {
             delay(500) 
            println ("Задача из глобальной области видимости") 
        }

         coroutineScope {
             delay(1500) 
            println ("Задача из области сопрограммы") 
        } 
    } 
    println ("Выполнение программы будет возобновлено") 
}
/* 
Вывод на консоль:

Выполнение программы будет заблокировано 
Задача из глобальной области видимости
Задача из runBlocking
Задача из области сопрограммы
Выполнение программы будет возобновлено
*/

Как видите, первое сообщение «Выполнение программы будет заблокировано», что нормально, это первая инструкция, а затем мы запускаем нашу runBlocking сопрограмму, и тут все становится интереснее!

Первое сообщение, которое мы получаем, это «Задача из глобальной области видимости», несмотря на то, что это не первое сообщение, которое мы пишем в нашей программе, потому что эта сопрограмма имеет наименьшую задержку, поэтому она первая напечатает сообщение. После этого у нас есть вторая сопрограмма runBlocking, а затем coroutineScope, которая является еще одной областью действия. После запуска всех этих сопрограмм, runBlocking завершится, и наша основная программа теперь сможет возобновится, и поэтому у нас есть сообщение «Выполнение программы будет возобновлено».

Context в корутинах

Контексты в сопрограммах очень привязаны к области видимости. Мы можем определить контекст как набор данных, к которым вы можете получить доступ в сопрограмме. Итак, у нас есть области, которые используются для управления сопрограммами, а контексты — это данные или переменные для этой сопрограммы. Наиболее часто используемые данные в Context:

  • Dispatchers: позволяет нам определить поток, в котором будет выполняться эта сопрограмма.
  • Job: позволяет нам управлять жизненным циклом сопрограммы.

Начнем с простой задачи, давайте дадим имя нашей сопрограмме.

fun main() { 
    runBlocking { 
        launch (CoroutineName("my-coroutine")) { 
            println ("это запускается из ${coroutineContext[CoroutineName.Key]}") 
        } 
    }
 }
/* 
Вывод на консоль:

это запускается из CoroutineName(my-coroutine)
*/

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

Исходный код GlobalScope, содержащий переменную coroutineContext

Функция suspend

Функция suspend (приостанавливаемая функция) похожа на любую другую функцию, но имеет ключевое слово suspend. Чтобы вызвать функцию внутри сопрограммы, эта функция должна быть функцией suspend. Давайте напишем небольшую программу, которая запускает некоторые функции suspend внутри сопрограммы, и посмотрим на результат. Программа вызовет две функции suspend внутри сопрограммы, и каждая функция будет увеличивать переменную с именем functionCalls, которая будет отслеживать количество функций suspend, вызванных в этой программе.

var functionCalls = 0 

fun main() { 
    println ("запуск программы с вызовами функций $ functionCalls ") 
    runBlocking {
         sayHello() 
        sayWorld() 
    } 
    println ("программа завершена вызовами функций $ functionCalls ") 
} 

suspend fun sayHello() { 
    print ("Hello") 
    functionCalls ++ 
} 

suspend fun sayWorld(){ 
    println ("World") 
    functionCalls ++ 
}
/* 
Вывод на консоль:

запуск программы с вызовами функций 0
Hello World 
программа завершена вызовами функций 2
*/

Обратите внимание, как легко обновлять переменную в разных потоках. Мы просто вызываем его из любой области, и нам не нужно беспокоиться об обмене потоками.

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

Job

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

job1(){
    job2()
    job3(){
        job4()
    }
}

job1 является родительским для job2 и job3.
job4 является дочерним элементом job3 , который является дочерним элементом job1.
Ссылку на job сопрограммы можно получить так:

val job = GlobalScope.launch { 
    //Ваш код
}

Вы можете проверить, каковы методы job. Для этого есть функции join() и cancel()Join() позволяет нам присоединиться к определенной области job или сопрограмме, а cancel() позволяет нам отменить задание. Eсли вы отмените задание, все задания в иерархии (её потомки) будут отменены.

На самом деле у job есть и другие методы, некоторые из которых связаны с жизненным циклом job. Например invoqueOnCompletion{}. Как только job будет выполнено или отменено, блок внутри этого метода будет выполнен. Давайте рассмотрим это на примере:

fun main() { 
    runBlocking {
         val job1 = launch {
             delay(1000) 
            println ("задание1 запущено") 
        }

         job1.invokeOnCompletion { println ("задание1 завершено") } 
    } 
    println ("программа завершена") 
}

/* 
Вывод на консоль:

задание1 запущено 
задание1 завершено 
программа завершена
*/

В этом примере мы запускаем сопрограмму в runBlocking, задерживаемся на одну секунду, а затем печатаем сообщение. Затем мы вызываем метод invokeOnCompletion{} и печатаем сообщение после завершения задания

Теперь попробуйте добавить job1.cancel() сразу после job1.invokeOnCompletion{...} и посмотрите, что произойдет. Вы заметите, что сообщение «job1 запущено» не будет напечатано, потому что мы отменили задание, поэтому задание не продолжит свое выполнение и будет вызвано invokeOnCompletion{} .

Dispatchers (диспетчер)

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

Итак, давайте посмотрим, как мы можем определить наши собственные потоки с помощью Dispatcher. Чтобы выбрать правильный диспетчера, мы должны знать, какие диспетчеры есть в сопрограмме и в чем разница между ними.

  • Dispatchers.IO: похоже на io() планировщик в RxJava, он используется, когда у нас есть чтение/запись файла или получение данных по сети. В основном используется для чтения/записи данных.
  • Dispatchers.Default: как computation() в RxJava, используется для работ, связанных с интенсивной работой процессора. Например, применение фильтра к изображению.
  • Dispatchers.Main: для работ в основном потоке. В приложении для Android любые обновления пользовательского интерфейса должны быть в MainThread (основном потоке), поэтому не забудьте включить Dispatchers.Main, чтобы обновить свой пользовательский интерфейс. Мы поговорим о том, как мы можем переключаться между диспетчерами в одной и той же сопрограмме!
  • Dispatchers.Unconfined когда вы определяете этот диспетчер, сопрограмма будет выполняться на тех же диспетчерах, из которых она была вызвана.

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

Вот код, в котором мы запускаем разные сопрограммы в разных диспетчерах:

fun main() { 
    runBlocking { 
        //launch (Dispatchers.Main) { 
        //    println ("main runBlocking: я работаю в потоке ${Thread.currentThread().name }") 
        //} 
        launch { 
            println("main runBlocking: я работа в потоке ${Thread.currentThread().name }") 
        } 
        launch(Dispatchers.Unconfined) { 
            println ("Unconfined: я работаю в потоке ${Thread.currentThread().name }") 
        } 
        launch(Default.IO) { 
            println ("IO: я работаю в потоке ${Thread.currentThread().name }")
        } 
        launch (Dispatchers.Default) { 
            println ("Default: я работаю в потоке ${Thread.currentThread().name }") 
        } 
        launch(newSingleThreadContext ("MyOwnThread")) { 
            println ("newSingleThreadContext: я работа в потоке ${Thread.currentThread().name }") 
        } 
    }
 }
/* 
Вывод на консоль:

Unconfined: я работаю в потоке main 
IO: я работаю в потоке DefaultDispatcher-worker-1 
Default: я работаю в потоке DefaultDispatcher-worker-1 
newSingleThreadContext: я работаю в потоке MyOwnThread 
main runBlocking: я работа в основном потоке
*/

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

Как видите, Unconfined выполняется в основном потоке. 
IO и Default— в разных рабочих потоках.
newSingleThreadContext, который мы не упомянули в списке диспетчеров выше. Это способ сказать сопрограмме запустить новый поток для этой job. Не рекомендуется использовать его, поскольку выделенный поток является очень тяжелым ресурсом.

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

Поделись с друзьями:
Если вам понравилась статья, подписывайтесь на наши социальные сети.

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

18 − 1 =