Kotlin Coroutines (сопрограммы) полный анализ (1), введение в сопрограммы

Java Kotlin

Эта статья основана на Kotlin v1.2.31, Kotlin-Coroutines v0.22.5.

В Kotlin введена концепция Coroutine (сопрограммы), которая может помочь в написании асинхронного кода, и в настоящее время она все еще является экспериментальной. Подробной информации о сопрограммах в Китае относительно мало, поэтому я планирую написать серию статей по полному анализу Kotlin Coroutines (сопрограмм), надеясь помочь вам лучше понять сопрограммы. Это первая статья из серии статей, в которых кратко представлены характеристики сопрограмм и некоторые основные понятия. Основная цель сопрограмм — упростить асинхронное программирование, поэтому начните с того, зачем вам нужны сопрограммы для написания асинхронного кода.

1. Зачем нужны сопрограммы?

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

fun requestToken(): Token {
    // makes request for a token & waits
    return token // returns result when received 
}
fun createPost(token: Token, item: Item): Post {
    // sends item to the server & waits
    return post // returns resulting post 
}
fun processPost(post: Post) {
    // does some local processing of result
}

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

1.1 Обратный вызов

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

fun requestTokenAsync(cb: (Token) -> Unit) { ... }
fun createPostAsync(token: Token, item: Item, cb: (Post) -> Unit) { ... }
fun processPost(post: Post) { ... }
fun postItem(item: Item) {
    requestTokenAsync { token ->
        createPostAsync(token, item) { post ->
            processPost(post)
        }
    }
}

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

1.2 Future

CompletableFuture, представленный в Java 8, может связывать несколько задач вместе, чтобы избежать проблемы многоуровневой вложенности.

fun requestTokenAsync(): CompletableFuture<Token> { ... }
fun createPostAsync(token: Token, item: Item): CompletableFuture<Post> { ... }
fun processPost(post: Post) { ... }
fun postItem(item: Item) {
    requestTokenAsync()
            .thenCompose { token -> createPostAsync(token, item) }
            .thenAccept { post -> processPost(post) }
            .exceptionally { e ->
                e.printStackTrace()
                null
            }
}

В приведенном выше коде используются коннекторы для соединения трех задач, и последняяexceptionallyЭтот метод также может единообразно обрабатывать исключения, но его можно использовать только в Java 8 и более поздних версиях.

1.3 Программирование приема

Способ CompletableFuture чем-то похож на цепочку вызовов серии Rx, которая также является наиболее рекомендуемой практикой в ​​настоящее время.

fun requestToken(): Token { ... }
fun createPost(token: Token, item: Item): Post { ... }
fun processPost(post: Post) { ... }
fun postItem(item: Item) {
    Single.fromCallable { requestToken() }
            .map { token -> createPost(token, item) }
            .subscribe(
                    { post -> processPost(post) }, // onSuccess
                    { e -> e.printStackTrace() } // onError
            )
}

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

1.4 Сопрограммы

Вот код, использующий сопрограммы Kotlin:

suspend fun requestToken(): Token { ... }   // 挂起函数
suspend fun createPost(token: Token, item: Item): Post { ... }  // 挂起函数
fun processPost(post: Post) { ... }
fun postItem(item: Item) {
    launch {
        val token = requestToken()
        val post = createPost(token, item)
        processPost(post)
        // 需要异常处理,直接加上 try/catch 语句即可
    }
}

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

2. Что такое сопрограмма

2.1 Введение в Gradle

dependencies {
    // Kotlin
    compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
    // Kotlin Coroutines
    compile 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.22.5'
}
kotlin {
    experimental {
        coroutines 'enable'     // avoid compiler reports a warning every time when we use the experimental coroutines
    }
}

2.2 Определение сопрограммы

Сначала посмотрите на описание официального документа:

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

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

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

3. Основные понятия сопрограмм

Давайте представим некоторые основные концепции сопрограмм на примере сопрограмм выше:

3.1 Функция приостановки

suspend fun requestToken(): Token { ... }   // 挂起函数
suspend fun createPost(token: Token, item: Item): Post { ... }  // 挂起函数
fun processPost(post: Post) { ... }

requestTokenиcreatePostфункционировать доsuspendФлаг модификатора, указывающий, что обе функции приостанавливают работу. Приостановленные функции могут принимать аргументы и возвращать значения так же, как и обычные функции, но вызывающая функция может приостанавливать сопрограмму (если результат соответствующего вызова уже доступен, библиотека может решить продолжить без приостановки),Когда функция приостановки приостанавливает сопрограмму, она не блокирует поток, в котором находится сопрограмма.. После завершения выполнения приостановленной функции сопрограмма будет возобновлена, а последующий код продолжит выполнение. Но функции приостановки можно вызывать только в сопрограммах или других функциях приостановки. На самом деле, чтобы запустить сопрограмму, должна быть хотя бы одна функция приостановки, которая обычно является приостановкой. лямбда-выражения. такsuspendМодификаторы могут помечать обычные функции, функции расширения и лямбда-выражения.

Приостановленные функции можно вызывать только в сопрограммах или других приостанавливающих функциях, как в приведенном выше примере.launchФункция создает сопрограмму.

fun postItem(item: Item) {
    launch { // 创建一个新协程
        val token = requestToken()
        val post = createPost(token, item)
        processPost(post)
        // 需要异常处理,直接加上 try/catch 语句即可
    }
}

launchфункция:

public actual fun launch(
    context: CoroutineContext = DefaultDispatcher,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    parent: Job? = null,
    block: suspend CoroutineScope.() -> Unit
): Job

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

3.1 CoroutineContext

CoroutineContext, контекст сопрограммы, представляет собой набор элементов, в основном включающий элементы Job и CoroutineDispatcher, которые могут представлять сценарий сопрограммы.

EmptyCoroutineContext представляет пустой контекст сопрограммы.

3.2 CoroutineDispatcher

CoroutineDispatcher, планировщик сопрограмм, определяет поток или пул потоков, в котором находится сопрограмма. Он может указать, что сопрограмма выполняется в определенном потоке, пуле потоков или не выполняется ни в каком потоке (поэтому сопрограмма будет выполняться в текущем потоке).coroutines-coreВ CoroutineDispatcher есть две стандартные реализации CommonPool и Unconfined: Unconfined означает, что ни один поток не указан.

launchв определении функцииDefaultDispatcherфактическиCommonPool,CommonPoolПланировщик сопрограмм, указанный поток которого является общим пулом потоков. А CoroutineDispatcher реализует интерфейс CoroutineContext, поэтому его можно указать напрямуюcontext: CoroutineContext = DefaultDispatcher, на самом деле все элементы контекста сопрограммы реализуют Интерфейс CoroutineContext.

3.3 Job & Deferred

Работа, задача инкапсулирует логику кода, которая должна быть выполнена в сопрограмме. Задание может быть отменено и имеет простой жизненный цикл, имеет три состояния:

State [isActive] [isCompleted] [isCancelled]
New (optional initial state) false false false
Active (default initial state) true false false
Completing (optional transient state) true false false
Cancelling (optional transient state) false false true
Cancelled (final state) false true true
Completed (final state) false true false

Когда задание завершается, возвращаемого значения нет. Если вам нужно возвращаемое значение, вы должны использовать Deferred, который является подклассом Job.public actual interface Deferred<out T> : Job. Deferred имеет дополнительное состояние isCompletedExceptionally:

State [isActive] [isCompleted] [isCompletedExceptionally] [isCancelled]
New (optional initial state) false false false false
Active (default initial state) true false false false
Completing (optional transient state) true false false false
Cancelling (optional transient state) false false false true
Cancelled (final state) false true true true
Resolved (final state) false true false false
Failed (final state) false true true false

3.4 Coroutine builders

launchФункции относятся к сборщику сопрограмм Coroutine builders, и в Kotlin есть несколько других Builders, отвечающих за создание сопрограмм.

3.4.1 launch {}

launch {}Это наиболее часто используемый конструктор сопрограмм, который не блокирует текущий поток, создает новую сопрограмму в фоновом режиме, а также может указывать планировщик сопрограмм, например тот, который обычно используется в Android.launch(UI) {}.

fun postItem(item: Item) {
    launch(UI) { // 在 UI 线程创建一个新协程
        val token = requestToken()
        val post = createPost(token, item)
        processPost(post)
    }
}
3.4.2 runBlocking {}

runBlocking {}Является ли создание новой сопрограммы при блокировке текущего потока до тех пор, пока сопрограмма не завершится. Это не следует использовать в сопрограммах, в основном дляmainфункция и дизайн теста.

fun main(args: Array<String>) = runBlocking<Unit> { // start main coroutine
    launch { // launch 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
}
class MyTest {
    @Test
    fun testMySuspendingFunction() = runBlocking<Unit> {
        // here we can use suspending functions using any assertion style that we like
    }
}
3.4.3 withContext {}

withContext {}Не создает новую сопрограмму, запускает приостановленный блок кода на указанной сопрограмме и приостанавливает сопрограмму до тех пор, пока блок кода не будет выполнен до завершения.

3.4.4 async {}

async {}Может добиться того же эффекта, что и конструктор запуска, создав новую сопрограмму в фоновом режиме, с той лишь разницей, что она имеет возвращаемое значение, т.к.async {}Возвращаемый тип — отложенный.

fun main(args: Array<String>) = runBlocking<Unit> { // start main coroutine
    val time = measureTimeMillis {
        val one = async { doSomethingUsefulOne() }  // start async one coroutine without suspend main coroutine
        val two = async { doSomethingUsefulTwo() }  // start async two coroutine without suspend main coroutine
        println("The answer is ${one.await() + two.await()}") // suspend main coroutine for waiting two async coroutines to finish
    }
    println("Completed in $time ms")
}

Получатьasync {}Возвращаемое значение должно быть передано черезawait()функция, это также функция приостановки, при вызове она приостанавливает текущую сопрограмму до тех пор, пока код в асинхронном режиме не будет выполнен и не вернет определенное значение.

4. Резюме

Корутины Kotlin могут значительно упростить асинхронное программирование, и я с нетерпением жду того дня, когда он будет официально выпущен. Хотя мне было трудно учиться, когда я впервые начал контактировать с ним, я верю, что обязательно влюблюсь в него после того, как побуду с ним в контакте какое-то время. Рекомендуется также просмотреть рекомендуемые материалы ниже, чтобы быстрее понять сопрограмму.

Рекомендуемое чтение: