April 14

Рутины к Корутинам: как Котлин произвел революцию в асинхронном программировании

· 4 мин чтения · 30 марта 2025

Основы: Главная рутина и подпрограммы

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

Что такое главная рутина?

Главная рутина (часто называемая main()) — это:

Точка входа каждой программы

Первая функция, выполняемая при запуске кода

Организует рабочий процесс программы

• В Kotlin/Java/C-подобных языках выглядит так:

fun main() {
    // Программа начинает выполнение здесь
    first()
    second()
}

Ключевые характеристики:

1. Обязательна — Каждая исполняемая программа должна иметь её

2. Родительская функция — Вызывает другие подпрограммы

3. Контроллер жизненного цикла — Определяет порядок операций

Что такое подпрограммы?

Подпрограммы (также называемые функциями/методами) — это:

Повторно используемые блоки кода для конкретных задач

Вызываются главной рутиной или другими подпрограммами

Модулируют код в логические единицы

fun first() {
    // Выполняет задачу A
}

fun second() {
    // Выполняет задачу B
}

Ключевые характеристики:

1. Единая ответственность — Каждая должна делать одну вещь хорошо

2. Вызываемость — Может запускаться многократно

3. Основаны на стеке — Следуют принципу LIFO (последним вошёл — первым вышел)

Пример с блокировкой

fun main() {
    first()  // подпрограмма
    second() // подпрограмма
}

fun first() {
    var first = 1
    while (true) {
        println("first: ${first++}")
    }
}

fun second() {
    var second = 1
    while (true) {
        println("second: ${second++}")
    }
}

Что происходит на самом деле:

1. Программа запускается → main() начинает выполняться

2. main() вызывает first() → выполнение переходит к first()

3. first() входит в бесконечный цикл → никогда не возвращается

4. second() никогда не вызывается, потому что управление не возвращается в main()

Критическое ограничение

Это демонстрирует фундаментальное ограничение программирования:

Подпрограммы блокируют — Вызывающий ждёт завершения

Нет параллелизма — В любой момент выполняется только одна функция

Зависит от порядка — Строго последовательное выполнение

Визуализация стека вызовов

Такой блокирующий подход решается корутинами, которые предоставляют:

• Приостанавливаемые функции

• Неблокирующие операции

• Параллельное выполнение

Решение с корутинами

fun main(): Unit = runBlocking {
    launch { first() }  // корутина 1
    launch { second() } // корутина 2
}

suspend fun first() {
    var first = 1
    while (true) {
        println("first: ${first++}")
        delay(2000) // Точка приостановки
    }
}

suspend fun second() {
    var second = 1
    while (true) {
        println("second: ${second++}")
        delay(1000) // Точка приостановки
    }
}

Ключевые отличия от подпрограмм

1. Неблокирующее параллельное выполнение

• Обе функции теперь выполняются параллельно, а не последовательно

• Вывод будет чередоваться: «second» печатается в два раза чаще, чем «first»

• Ни одна функция не блокирует другую

2. Магия точек приостановки

delay() — это функция приостановки, которая останавливает выполнение без блокировки

• При встрече такой функции корутина:

◦ Освобождает поток

◦ Планирует возобновление

◦ Позволяет выполняться другим корутинам

3. Структурированный параллелизм

runBlocking создаёт область видимости для корутин

launch запускает новые корутины как дочерние элементы этой области

• Все корутины автоматически отменяются при завершении области видимости

Внутренний механизм

Хронология выполнения корутин

Почему это важно?

1. Эффективность ресурсов

› Традиционные потоки: ~1 МБ стека на поток

› Корутины: ~50 байт на приостановленную корутину

2. Упрощённый параллелизм

› Нет ада колбэков

› Последовательно выглядящий код с асинхронным выполнением

3. Отзывчивые приложения

› Главный поток никогда не блокируется

› Лёгкая реализация:

› Параллельные сетевые запросы

› Анимации с задержками

› Фоновая обработка

Визуальное сравнение

Выполнение подпрограмм

main()
└── first() (выполняется вечно)
    └── second() (никогда не достигается)

Выполнение корутин

runBlocking
├── launch { first() } (приостанавливается/возобновляется)
└── launch { second() } (приостанавливается/возобновляется)

Заключение

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

Надеюсь, статья вам понравилась. Удачного кодинга!

Опубликовано в ProAndroidDev

#coroutines #kotlin #android