Spec-Zone.ru › Kotlin 1.4

Содержание

  • Основы корутин
    • Ваша первая корутина
    • Переход между блокирующим и неблокирующим мирами
    • Ожидание завершения задачи
    • Структурированная конкурентность
    • Строитель области
    • Рефакторинг: выделение функции
    • Корутины — легковесные
    • Глобальные корутины подобны демонам

Основы корутин

В этом разделе рассматриваются основные понятия корутин.

Ваша первая корутина

Запустите следующий код:

import kotlinx.coroutines.*

fun main() {
    GlobalScope.launch { // launch a new coroutine in background and continue
        delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
        println("World!") // print after delay
    }
    println("Hello,") // main thread continues while coroutine is delayed
    Thread.sleep(2000L) // block main thread for 2 seconds to keep JVM alive
}

Полный код доступен здесь.

Вы увидите следующий результат:

Hello,
World!

По существу, корутины — это легковесные потоки. Они запускаются с помощью launch — строителя корутин — в контексте некоторой CoroutineScope. В данном случае новая корутина запускается в GlobalScope, что означает, что ее жизненный цикл ограничен только жизненным циклом всего приложения.

Вы можете добиться того же результата, заменив GlobalScope.launch { ... } на thread { ... }, а delay(...) на Thread.sleep(...). Попробуйте (не забудьте импортировать kotlin.concurrent.thread).

Если вы начнете с замены GlobalScope.launch на thread, компилятор выдаст следующую ошибку:

Error: Kotlin: Suspend functions are only allowed to be called from a coroutine or another suspend function

Это связано с тем, что delay — это специальная подвешивающая функция, которая не блокирует поток, а подвешивает корутину, и ее можно использовать только из корутины.

Переход между блокирующим и неблокирующим мирами

Первый пример смешивает неблокирующие delay(...) и блокирующие Thread.sleep(...) операции в одном коде. Легко потерять из виду, какая из них блокирующая, а какая нет. Давайте явно укажем блокирующие операции, используя строитель корутин runBlocking:

import kotlinx.coroutines.*

fun main() { 
    GlobalScope.launch { // launch a new coroutine in background and continue
        delay(1000L)
        println("World!")
    }
    println("Hello,") // main thread continues here immediately
    runBlocking {     // but this expression blocks the main thread
        delay(2000L)  // ... while we delay for 2 seconds to keep JVM alive
    } 
}

Полный код доступен здесь.

Результат остается тем же, но этот код использует только неблокирующие delay. Главный поток, вызывающий runBlocking, блокируется до завершения корутины внутри runBlocking.

Этот пример также можно переписать более идиоматичным способом, используя runBlocking для обертывания выполнения основной функции:

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> { // start main coroutine
    GlobalScope.launch { // launch a new coroutine in background and continue
        delay(1000L)
        println("World!")
    }
    println("Hello,") // main coroutine continues here immediately
    delay(2000L)      // delaying for 2 seconds to keep JVM alive
}

Полный код доступен здесь.

Здесь runBlocking<Unit> { ... } работает как адаптер, используемый для запуска основной корутины верхнего уровня. Мы явно указываем ее Unit тип возвращаемого значения, так как правильно сформированная main функция в Kotlin должна возвращать Unit.

Это также способ написания юнит-тестов для подвешенных функций:

class MyTest {
    @Test
    fun testMySuspendingFunction() = runBlocking<Unit> {
        // here we can use suspending functions using any assertion style that we like
    }
}

Ожидание завершения задачи

Задержка на время работы другой корутины — не лучший подход. Давайте явно дождемся (неблокирующим образом) завершения фоновой задачи Job, которую мы запустили:

import kotlinx.coroutines.*

fun main() = runBlocking {
//sampleStart
    val job = GlobalScope.launch { // launch a new coroutine and keep a reference to its Job
        delay(1000L)
        println("World!")
    }
    println("Hello,")
    job.join() // wait until child coroutine completes
//sampleEnd    
}

Полный код доступен здесь.

Теперь результат по-прежнему тот же, но код основной корутины никак не привязан к продолжительности работы фоновой задачи. Гораздо лучше.

Структурированная конкурентность

Для практического использования корутин все еще есть что улучшить. Когда мы используем GlobalScope.launch, мы создаем корутину верхнего уровня. Несмотря на то, что она легковесная, она все еще потребляет некоторые ресурсы памяти во время работы. Если мы забудем сохранить ссылку на запущенную корутину, она продолжит работу. Что если код в корутине зависнет (например, мы неосмотрительно задерживаемся слишком долго), а что если мы запустили слишком много корутин и закончились памятью? Ручное хранение ссылок на все запущенные корутины и их join — подвержено ошибкам.

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

В нашем примере у нас есть функция main, преобразованная в корутину с помощью runBlocking. Каждый строитель корутин, включая runBlocking, добавляет экземпляр CoroutineScope в область действия своего блока кода. Мы можем запускать корутины в этой области действия, не принудительно join их, так как внешняя корутина (runBlocking в нашем примере) не завершается до тех пор, пока не завершатся все корутины, запущенные в ее области действия. Таким образом, мы можем упростить наш пример:

import kotlinx.coroutines.*

fun main() = runBlocking { // this: CoroutineScope
    launch { // launch a new coroutine in the scope of runBlocking
        delay(1000L)
        println("World!")
    }
    println("Hello,")
}

Полный код доступен здесь.

Строитель области

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

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

Это можно продемонстрировать на следующем примере:

import kotlinx.coroutines.*

fun main() = runBlocking { // this: CoroutineScope
    launch { 
        delay(200L)
        println("Task from runBlocking")
    }
    
    coroutineScope { // Creates a coroutine scope
        launch {
            delay(500L) 
            println("Task from nested launch")
        }
    
        delay(100L)
        println("Task from coroutine scope") // This line will be printed before the nested launch
    }
    
    println("Coroutine scope is over") // This line is not printed until the nested launch completes
}

Полный код доступен здесь.

Обратите внимание, что сразу после сообщения "Задача из области действия корутины" (в ожидании завершения вложенного запуска) выполняется и выводится "Задача из runBlocking", — хотя coroutineScope еще не завершен.

Рефакторинг: выделение функции

Давайте вынесем блок кода внутри launch { ... } в отдельную функцию. При выполнении переименования «Выделить функцию» на этом коде, вы получите новую функцию с модификатором suspend. Это ваша первая функция приостановки. Функции приостановки могут использоваться внутри сопрограмм, как и обычные функции, но их дополнительная возможность заключается в том, что они могут, в свою очередь, использовать другие функции приостановки (например, delay в этом примере), чтобы приостановить выполнение сопрограммы.

import kotlinx.coroutines.*

fun main() = runBlocking {
    launch { doWorld() }
    println("Hello,")
}

// this is your first suspending function
suspend fun doWorld() {
    delay(1000L)
    println("World!")
}

Вы можете получить весь код здесь.

Но что, если выделенная функция содержит билдер сопрограмм, который вызывается в текущем контексте? В этом случае модификатор suspend выделенной функции недостаточно. Преобразование doWorld в расширяющий метод для CoroutineScope является одним из решений, но оно может не всегда быть применимо, так как не делает API более понятным. Идиоматическое решение заключается в наличии либо явного CoroutineScope в качестве поля в классе, содержащем целевую функцию, либо неявного, когда внешний класс реализует CoroutineScope. В крайнем случае, можно использовать CoroutineScope(coroutineContext), но такой подход структурно небезопасен, потому что вы больше не имеете контроля над областью выполнения этого метода. Только закрытые API могут использовать этот билдер.

Сопрограммы — лёгковесные

Запустите следующий код:

import kotlinx.coroutines.*

fun main() = runBlocking {
    repeat(100_000) { // launch a lot of coroutines
        launch {
            delay(5000L)
            print(".")
        }
    }
}

Вы можете получить весь код здесь.

Он запускает 100 000 сопрограмм, и после 5 секунд каждая сопрограмма выводит точку.

Теперь попробуйте это с потоками. Что произойдёт? (Скорее всего, ваш код вызовет ошибку недостатка памяти)

Глобальные сопрограммы подобны демонам-потокам

Следующий код запускает долго выполняющуюся сопрограмму в GlobalScope, которая выводит «Я сплю» дважды в секунду, а затем возвращается из основной функции после некоторой задержки:

import kotlinx.coroutines.*

fun main() = runBlocking {
//sampleStart
    GlobalScope.launch {
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
    }
    delay(1300L) // just quit after delay
//sampleEnd    
}

Вы можете получить весь код здесь.

Вы можете запустить и увидеть, что он выводит три строки и завершается:

I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...

Активные сопрограммы, запущенные в GlobalScope, не поддерживают процесс живым. Они подобны демонам-потокам.

© 2010–2020 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/reference/coroutines/basics.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API