Spec-Zone.ru › Kotlin 2

Композиция приостанавливающих функций

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

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

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

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, или если вызывается функция start её Job. Выполните следующий пример:

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 приводится здесь только для иллюстрации, поскольку он популярен в других языках программирования. Использовать этот стиль с корутинами Kotlin крайне не рекомендуется по причинам, объяснённым ниже.

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

GlobalScope — это деликатный API, который может привести к неожиданным последствиям, одно из которых будет рассмотрено ниже. Поэтому для использования GlobalScope с @OptIn(DelicateCoroutinesApi::class) необходимо явно предоставить соответствующее согласие.

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

// The result type of somethingUsefulTwoAsync is Deferred<Int>
@OptIn(DelicateCoroutinesApi::class)
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

@OptIn(DelicateCoroutinesApi::class)
fun somethingUsefulOneAsync() = GlobalScope.async {
    doSomethingUsefulOne()
}

@OptIn(DelicateCoroutinesApi::class)
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
}

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

Представьте, что между строкой 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()
}

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

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

Second child throws an exception
First child was cancelled
Computation failed with ArithmeticException
27 февраля 2025 г.
Корутины и каналы − руководствоКонтекст и диспетчеры корутин

© 2010–2026 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/composing-suspending-functions.html

Spec-Zone.ru

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