Spec-Zone.ru › Kotlin 1.4

Содержание

  • Составление приостанавливаемых функций
    • Последовательно по умолчанию
    • Конкурентно с использованием async
    • Отложенный запуск async
    • Функции в стиле async
    • Структурированная конкурентность с async

Составление приостанавливаемых функций

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

Последовательно по умолчанию

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

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) // pretend we are doing something useful here
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L) // pretend we are doing something useful here, too
    return 29
}

Что делать, если нам нужно, чтобы они вызывались последовательно — сначала doSomethingUsefulOne и затем doSomethingUsefulTwo, и вычислить сумму их результатов? На практике мы делаем это, если результат первой функции используется для принятия решения о необходимости вызова второй функции или о том, как ее вызвать.

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

import kotlinx.coroutines.*
import kotlin.system.*

fun main() = runBlocking<Unit> {
//sampleStart
    val time = measureTimeMillis {
        val one = doSomethingUsefulOne()
        val two = doSomethingUsefulTwo()
        println("The answer is ${one + two}")
    }
    println("Completed in $time ms")
//sampleEnd    
}

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) // pretend we are doing something useful here
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L) // pretend we are doing something useful here, too
    return 29
}

Полный код можно найти здесь.

Он выводит примерно следующее:

The answer is 42
Completed in 2017 ms

Конкурентно с использованием async

А что, если нет зависимостей между вызовами doSomethingUsefulOne и doSomethingUsefulTwo и мы хотим получить ответ быстрее, выполняя их конкурентно? Вот где async нам поможет.

По сути, async похож на launch. Он запускает отдельную сопрограмму, которая является лёгким потоком, работающим параллельно со всеми другими сопрограммами. Разница в том, что launch возвращает Job и не несёт никакого результирующего значения, а async возвращает Deferred — лёгкий неблокирующий фьючерс, представляющий собой обещание предоставить результат позже. Вы можете использовать .await() для получения конечного результата от отложенного значения, но Deferred также является Job, поэтому вы можете отменить его, если нужно.

import kotlinx.coroutines.*
import kotlin.system.*

fun main() = runBlocking<Unit> {
//sampleStart
    val time = measureTimeMillis {
        val one = async { doSomethingUsefulOne() }
        val two = async { doSomethingUsefulTwo() }
        println("The answer is ${one.await() + two.await()}")
    }
    println("Completed in $time ms")
//sampleEnd    
}

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) // pretend we are doing something useful here
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L) // pretend we are doing something useful here, too
    return 29
}

Полный код можно найти здесь.

Он выводит примерно следующее:

The answer is 42
Completed in 1017 ms

Это в два раза быстрее, потому что две сопрограммы выполняются одновременно. Обратите внимание, что конкурентность с сопрограммами всегда явная.

Отложенный запуск async

Можно сделать async ленивым, установив его start параметр в CoroutineStart.LAZY. В этом режиме сопрограмма запускается только тогда, когда её результат требуется с помощью await, или если её функция Job `start` вызвана. Запустите следующий пример:

import kotlinx.coroutines.*
import kotlin.system.*

fun main() = runBlocking<Unit> {
//sampleStart
    val time = measureTimeMillis {
        val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() }
        val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() }
        // some computation
        one.start() // start the first one
        two.start() // start the second one
        println("The answer is ${one.await() + two.await()}")
    }
    println("Completed in $time ms")
//sampleEnd    
}

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) // pretend we are doing something useful here
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L) // pretend we are doing something useful here, too
    return 29
}

Полный код можно найти здесь.

Он выводит примерно следующее:

The answer is 42
Completed in 1017 ms

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

Обратите внимание, что если мы просто вызовем await в println без предварительного вызова start на отдельных сопрограммах, это приведёт к последовательному поведению, так как await запускает выполнение сопрограммы и ждёт её завершения, что не является целевым использованием ленивости. Целевое использование async(start = CoroutineStart.LAZY) — замена стандартной функции lazy в тех случаях, когда вычисление значения включает приостанавливаемые функции.

Функции в стиле async

Мы можем определить функции в стиле async, которые вызывают doSomethingUsefulOne и doSomethingUsefulTwo асинхронно, используя конструктор сопрограмм async со явным указанием GlobalScope. Мы называем такие функции с суффиксом «…Async», чтобы подчеркнуть тот факт, что они только запускают асинхронные вычисления, и нужно использовать результирующее отложенное значение, чтобы получить результат.

// The result type of somethingUsefulOneAsync is Deferred<Int>
fun somethingUsefulOneAsync() = GlobalScope.async {
    doSomethingUsefulOne()
}

// The result type of somethingUsefulTwoAsync is Deferred<Int>
fun somethingUsefulTwoAsync() = GlobalScope.async {
    doSomethingUsefulTwo()
}

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

Следующий пример показывает их использование вне сопрограммы:

import kotlinx.coroutines.*
import kotlin.system.*

//sampleStart
// note that we don't have `runBlocking` to the right of `main` in this example
fun main() {
    val time = measureTimeMillis {
        // we can initiate async actions outside of a coroutine
        val one = somethingUsefulOneAsync()
        val two = somethingUsefulTwoAsync()
        // but waiting for a result must involve either suspending or blocking.
        // here we use `runBlocking { ... }` to block the main thread while waiting for the result
        runBlocking {
            println("The answer is ${one.await() + two.await()}")
        }
    }
    println("Completed in $time ms")
}
//sampleEnd

fun somethingUsefulOneAsync() = GlobalScope.async {
    doSomethingUsefulOne()
}

fun somethingUsefulTwoAsync() = GlobalScope.async {
    doSomethingUsefulTwo()
}

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) // pretend we are doing something useful here
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L) // pretend we are doing something useful here, too
    return 29
}

Полный код можно найти здесь.

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

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

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

Давайте возьмём пример Конкурентно с использованием async и выделим функцию, которая одновременно выполняет doSomethingUsefulOne и doSomethingUsefulTwo и возвращает сумму их результатов. Так как конструктор сопрограмм async определён как расширение для CoroutineScope, нам нужно иметь его в области видимости, и именно это предоставляет функция coroutineScope:

suspend fun concurrentSum(): Int = coroutineScope {
    val one = async { doSomethingUsefulOne() }
    val two = async { doSomethingUsefulTwo() }
    one.await() + two.await()
}

Таким образом, если что-то пойдёт не так внутри кода функции concurrentSum и она выбросит исключение, все сопрограммы, запущенные в её области видимости, будут отменены.

import kotlinx.coroutines.*
import kotlin.system.*

fun main() = runBlocking<Unit> {
//sampleStart
    val time = measureTimeMillis {
        println("The answer is ${concurrentSum()}")
    }
    println("Completed in $time ms")
//sampleEnd    
}

suspend fun concurrentSum(): Int = coroutineScope {
    val one = async { doSomethingUsefulOne() }
    val two = async { doSomethingUsefulTwo() }
    one.await() + two.await()
}

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) // pretend we are doing something useful here
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L) // pretend we are doing something useful here, too
    return 29
}

Полный код можно найти здесь.

Мы по-прежнему имеем одновременное выполнение обеих операций, как видно из вывода функции main выше:

The answer is 42
Completed in 1017 ms

Отмена всегда распространяется по иерархии сопрограмм:

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    try {
        failedConcurrentSum()
    } catch(e: ArithmeticException) {
        println("Computation failed with ArithmeticException")
    }
}

suspend fun failedConcurrentSum(): Int = coroutineScope {
    val one = async<Int> { 
        try {
            delay(Long.MAX_VALUE) // Emulates very long computation
            42
        } finally {
            println("First child was cancelled")
        }
    }
    val two = async<Int> { 
        println("Second child throws an exception")
        throw ArithmeticException()
    }
    one.await() + two.await()
}

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

Обратите внимание, как и первый async , и ожидающий родитель отменяются при ошибке одного из дочерних элементов (а именно, two):

Second child throws an exception
First child was cancelled
Computation failed with ArithmeticException

© 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/composing-suspending-functions.html

Spec-Zone.ru

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