Как работают корутины

Работа корутин в Kotlin под капотом основана на использовании преобразования кода в функции с продолжениями (continuations). Корутинная модель Kotlin реализуется на основе кооперативной многозадачности, при которой задачи приостанавливаются и возобновляются в зависимости от определённых условий (например, ожидание завершения сетевого запроса). Под капотом компилятор преобразует корутины в структуры с состоянием и кодом продолжения, которые вызываются по мере необходимости.

Основные компоненты работы корутин

  1. Suspending Functions (приостанавливающиеся функции):

    • При вызове таких функций они могут приостановить выполнение программы до тех пор, пока не будет получен результат.
    • Чтобы реализовать это, Kotlin использует объект Continuation, который хранит текущее состояние выполнения и позволяет возобновить выполнение кода в нужной точке.
  2. Coroutine Context (контекст корутины):

    • Контекст определяет диспетчер (например, Dispatchers.IO, Dispatchers.Main), на котором корутина будет выполняться.
    • Это позволяет управлять потоками выполнения.
  3. Coroutine Builder (создание корутин):

    • launch, async, withContext — это строительные блоки, которые позволяют создавать корутины и управлять их жизненным циклом.
import kotlinx.coroutines.*

fun main() = runBlocking {
    launch(Dispatchers.IO) {
        println("Coroutine started on thread: ${Thread.currentThread().name}")
        val result = longRunningTask()
        println("Result: $result")
    }
}

suspend fun longRunningTask(): Int {
    delay(1000)
    return 42
}

Здесь функция longRunningTask является приостанавливающейся функцией, которая использует delay для паузы выполнения на 1 секунду, а затем возвращает значение 42.

Как это компилируется под капотом

При компиляции этот код преобразуется в серию состояний, которые Kotlin поддерживает с помощью Continuation. Каждая приостанавливающаяся функция компилируется в обычную функцию, которая принимает объект Continuation как дополнительный параметр. Вот пример сгенерированного псевдокода (упрощённо) для функции longRunningTask:

public Object longRunningTask(Continuation<? super Integer> continuation) {
    if (continuation.label == 0) {
        continuation.label = 1;
        delay(1000, continuation);
        return COROUTINE_SUSPENDED;
    } else if (continuation.label == 1) {
        return 42;
    }
    return null;
}

Объяснение:

  • label используется для хранения состояния корутины.
  • Когда корутина приостанавливается (delay), управление возвращается, а выполнение кода продолжается, когда операция завершится.
  • Возвращаемое значение COROUTINE_SUSPENDED сигнализирует о том, что выполнение приостановлено.

Пример с приостановкой и возобновлением

Рассмотрим, что происходит с функцией longRunningTask():

  • В первый раз она вызывается и доходит до delay. Корутина приостанавливается, и текущее состояние сохраняется в объекте Continuation.
  • После паузы в 1 секунду корутина возобновляется и возвращает результат — 42.

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

Пример преобразования с использованием Continuation в Kotlin

suspend fun exampleContinuation(): Int = suspendCoroutine { continuation ->
    Thread {
        Thread.sleep(1000)
        continuation.resume(42)
    }.start()
}

Здесь мы вручную создаём корутину с использованием suspendCoroutine. Она приостанавливает выполнение до тех пор, пока поток не завершит работу, а затем продолжает выполнение с результатом.

Важные детали:

  • Метки состояний: В корутине используется переменная label для отслеживания текущего состояния. Каждый раз при возобновлении выполнения корутина проверяет эту метку, чтобы продолжить выполнение с нужного места.
  • Continuation: Объект Continuation содержит всю информацию о текущем состоянии выполнения, включая текущий контекст (например, диспетчер), метку состояния и промежуточные результаты.
  • COROUTINE_SUSPENDED: Это специальное значение, которое возвращается для приостановки выполнения корутины. Когда выполнение приостанавливается, корутина "выходит", а выполнение возобновляется позже, начиная с последнего приостановленного состояния.

Пример более сложного перехода между состояниями

Рассмотрим теперь случай с несколькими приостанавливающими функциями:

suspend fun complexCoroutine(): Int {
    println("Starting complexCoroutine")
    val first = suspendFunction(5)
    val second = suspendFunction(first)
    return second * 2
}

Компилированный код для этой функции будет включать несколько состояний:

public final Object complexCoroutine(Continuation<? super Integer> continuation) {
    Object result;
    switch (continuation.label) {
        case 0: {
            System.out.println("Starting complexCoroutine");
            continuation.label = 1;
            result = suspendFunction(5, continuation);
            if (result == COROUTINE_SUSPENDED) {
                return COROUTINE_SUSPENDED;
            }
        }
        case 1: {
            int first = (Integer) result;
            continuation.label = 2;
            result = suspendFunction(first, continuation);
            if (result == COROUTINE_SUSPENDED) {
                return COROUTINE_SUSPENDED;
            }
        }
        case 2: {
            int second = (Integer) result;
            return second * 2;
        }
        default: {
            throw new IllegalStateException("This coroutine has already completed");
        }
    }
}

Здесь происходит несколько переходов между состояниями:

  1. Переход в состояние 0: Начинается выполнение, вызывается первая приостанавливающая функция, выполнение приостанавливается.
  2. Переход в состояние 1: Когда первая функция завершится, результат используется для вызова второй приостанавливающей функции.
  3. Переход в состояние 2: После завершения второй функции результат удваивается и возвращается.

Таким образом, корутины под капотом работают через автоматическое управление состояниями с помощью объекта Continuation, а переходы между состояниями управляются внутренними метками (например, label), что делает возможным приостановку и возобновление выполнения функций.