🎯 Kotlin

कोटलिन में मल्टीथ्रेडिंग और एसिंक्रोनसी — एक विस्तृत गाइड

Что это такое и зачем? (аналогия из реальной жизни)

Представь, что ты — повар на кухне. Тебе нужно приготовить обед из трёх блюд: суп, салат и десерт. У тебя есть только одна плита и одни руки. Как ты будешь готовить?

Последовательный способ (без многопоточности): ты ставишь суп вариться, ждёшь 40 минут, пока он сварится, потом режешь салат 15 минут, потом печёшь десерт 30 минут. Итого — 85 минут. Всё это время ты занят, но большую часть времени просто стоишь и ждёшь, пока суп закипит. Это неэффективно.

Многопоточный способ: ты нанимаешь двух помощников. Один варит суп, второй режет салат, третий печёт десерт. Все работают одновременно. Обед готов за 40 минут (самое долгое блюдо). Но теперь у тебя три повара, которые могут мешать друг другу на одной кухне, брать одни и те же продукты или инструменты.

Асинхронный способ: ты остаёшься один, но меняешь тактику. Ты ставишь суп на плиту и, пока он варится (тебе не нужно стоять рядом), ты режешь салат. Когда суп закипел — ты на секунду отвлекаешься, убавляешь огонь и продолжаешь резать салат. Потом ставишь десерт в духовку и, пока он печётся, нарезаешь салат. Ты не ждёшь — ты переключаешься между задачами, когда одна из них требует ожидания.

Важно: Многопоточность — это про одновременное выполнение (параллельно). Асинхронность — про неблокирующее ожидание (ты не сидишь сложа руки, пока ждёшь).

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

Теория: как это работает изнутри

Потоки (Threads) — база

Поток — это как отдельный рабочий. У каждого потока есть свой стек вызовов и своя очередь команд. Операционная система (Windows, Linux, Android) умеет переключаться между потоками так быстро, что кажется, будто они работают одновременно. На самом деле, если у процессора 4 ядра, то одновременно могут работать только 4 потока. Остальные просто ждут своей очереди.

Проблемы потоков

  • Дороговизна: Создать поток — это как нанять нового повара. Нужно выделить память (обычно 1 МБ на стек), настроить контекст. 10 000 потоков убьют любую программу.
  • Состояние гонки (Race Condition): Два потока одновременно меняют одну переменную. Результат непредсказуем.
  • Deadlock (Взаимная блокировка): Поток A ждёт ресурс, который занят потоком B, а поток B ждёт ресурс, занятый потоком A. Оба застыли навечно.

Корутины (Coroutines) — эволюция

Корутины в Kotlin — это не потоки операционной системы. Это лёгкие потоки на уровне языка. Представь, что у тебя один повар (один поток), но у него 100 блокнотов с рецептами (корутины). Он работает над одним рецептом, пока не упрётся в ожидание (например, «поставить суп вариться на 10 минут»). В этот момент он откладывает этот блокнот, берёт следующий и продолжает работу. Когда суп сварился, он возвращается к первому блокноту.

Как это работает технически:

  1. Корутина выполняется в рамках какого-то потока (или нескольких).
  2. В точке delay() или await() корутина приостанавливается (suspend), не блокируя поток.
  3. Поток освобождается и может выполнять другие корутины.
  4. Когда время ожидания прошло или данные пришли, корутина возобновляется (resume) в любом свободном потоке (не обязательно в том же).
Ключевое отличие: Потоки блокируют ОС. Корутины блокируют только сами себя. 100 000 корутин работают на 4 потоках без проблем.

Dispatcher — диспетчер (распределитель)

Диспетчер решает, на каком потоке будет выполняться корутина. В Kotlin есть три основных:

ДиспетчерДля чегоПример
Dispatchers.DefaultТяжёлые вычисления (CPU-bound)Обработка изображений, сортировка
Dispatchers.IOРабота с файлами, сетью (I/O-bound)Чтение из БД, запрос к API
Dispatchers.MainUI-поток (только Android/Desktop)Обновление интерфейса

Синтаксис в Kotlin: все формы и варианты

1. Запуск корутины: launch и async

launch — запускает корутину, которая ничего не возвращает (как void). Это как «запустить таймер и забыть».

async — запускает корутину, которая возвращает результат через Deferred. Это как «заказать пиццу и получить номер заказа, по которому потом забрать».

2. Suspend-функции

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

3. Структура: runBlocking

Мост между обычным миром и миром корутин. runBlocking блокирует текущий поток до завершения всех корутин внутри. Используется только в main() или тестах. В реальном приложении (Android, Spring) он не нужен — там уже есть готовый контекст корутин.

4. Отмена и тайм-ауты

Корутины можно отменять. Если пользователь ушёл с экрана, все корутины, связанные с этим экраном, должны быть отменены. Для этого есть Job и CoroutineScope. Также есть withTimeout() — если корутина не успела за 5 секунд, она выбрасывает исключение.

5. Каналы (Channels)

Способ передачи данных между корутинами. Одна корутина отправляет (send), другая принимает (receive). Работает как очередь.

Примеры кода — от простого к сложному

Пример 1: Первая корутина

import kotlinx.coroutines.*

fun main() = runBlocking {
    // runBlocking создаёт блокирующий мост.
    // Внутри него мы можем вызывать suspend-функции.

    launch {
        // launch запускает новую корутину.
        // Она будет работать параллельно с остальным кодом.
        delay(1000L) // Приостанавливаемся на 1 секунду (не блокируя поток)
        println("Мир!") // Печатаем после задержки
    }

    println("Привет,") // Печатается сразу
    delay(1500L) // Ждём, чтобы корутина успела завершиться
}
// Вывод:
// Привет,
// Мир!

Пример 2: async и получение результата

import kotlinx.coroutines.*

fun main() = runBlocking {
    // async возвращает Deferred — объект, который хранит будущий результат
    val deferred: Deferred<Int> = async {
        // Симулируем долгий расчёт
        delay(1000L)
        42 // Последнее выражение — это результат
    }

    println("Ждём результат...")
    // .await() приостанавливает корутину, пока результат не будет готов
    val result: Int = deferred.await()
    println("Ответ: $result") // Ответ: 42
}

Пример 3: Две параллельные задачи

import kotlinx.coroutines.*

fun main() = runBlocking {
    // Запускаем две корутины параллельно
    val result1 = async {
        delay(1000L)
        "Результат 1"
    }
    val result2 = async {
        delay(1500L)
        "Результат 2"
    }

    // Ждём оба результата. Общее время — 1.5 секунды, а не 2.5!
    println("${result1.await()} и ${result2.await()}")
}

Пример 4: withContext — переключение диспетчера

import kotlinx.coroutines.*

fun main() = runBlocking {
    println("Начало в потоке: ${Thread.currentThread().name}")

    // withContext переключает выполнение на другой диспетчер
    val result = withContext(Dispatchers.IO) {
        // Этот блок выполняется в IO-потоке
        println("Работа в потоке: ${Thread.currentThread().name}")
        // Симулируем чтение файла
        Thread.sleep(500L) // Здесь sleep — это плохо, но для примера
        "Данные из файла"
    }

    println("Результат: $result")
    println("Конец в потоке: ${Thread.currentThread().name}")
}

Пример 5: Отмена корутины

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job: Job = launch {
        repeat(1000) { i ->
            println("Работаю... $i")
            delay(500L) // Точка приостановки — здесь корутина проверяет отмену
        }
    }

    delay(1300L) // Даём поработать 1.3 секунды
    println("Надоело, отменяю!")
    job.cancel() // Отменяем корутину
    job.join()   // Ждём, пока она завершится (необязательно, но правильно)
    println("Готово")
}
// Вывод:
// Работаю... 0
// Работаю... 1
// Работаю... 2
// Надоело, отменяю!
// Готово

Пример 6: Обработка ошибок в корутинах

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        try {
            // Код, который может выбросить исключение
            delay(100L)
            throw RuntimeException("Что-то пошло не так!")
        } catch (e: Exception) {
            println("Поймали ошибку: ${e.message}")
        }
    }
    job.join()
    println("Программа продолжает работу")
}

Пример 7: Каналы (Channels) — передача данных

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*

fun main() = runBlocking {
    // Создаём канал — очередь для передачи данных
    val channel = Channel<String>()

    // Корутина-отправитель
    launch {
        val items = listOf("Кот", "Собака", "Попугай")
        for (item in items) {
            println("Отправляю: $item")
            channel.send(item) // Отправляем в канал (приостанавливается, если канал полон)
            delay(300L)
        }
        channel.close() // Закрываем канал — сигнал, что данных больше не будет
    }

    // Корутина-получатель
    launch {
        // for автоматически получает данные, пока канал не закроется
        for (item in channel) {
            println("Получено: $item")
        }
        println("Канал закрыт, выходим")
    }
}

Сравнение с Python: чем похоже и чем отличается

ХарактеристикаKotlin (Coroutines)Python (asyncio)
Ключевые словаsuspend, launch, asyncasync def, await
Запуск корутиныlaunch { } или async { }asyncio.create_task() или await
ДиспетчерыВстроенные: Default, IO, MainEvent loop (один на поток)
Отменаjob.cancel() — кооперативнаяtask.cancel() — выбрасывает CancelledError
СтруктурированностьВстроенная (CoroutineScope)Ручная (нужно следить за задачами)
ПроизводительностьВысокая (на JVM)Средняя (GIL ограничивает)
КаналыВстроенные (Channel)asyncio.Queue
Параллелизм (на多个 ядрах)Да (Dispatchers.Default использует пул потоков)Нет (asyncio однопоточный, нужно multiprocessing)
Главное отличие: В Python asyncio — это библиотека, добавленная в 3.4. В Kotlin корутины — это часть языка, поддержанная на уровне компилятора. Kotlin-корутины легче, безопаснее и лучше интегрированы.

Типичные ошибки и подводные камни

1. Забыл про runBlocking или scope

Если ты вызываешь launch вне корутины, компилятор выдаст ошибку. Всегда нужен CoroutineScope.

2. Блокирующие вызовы внутри корутины

Никогда не используй Thread.sleep() внутри корутины! Это блокирует весь поток, а не только корутину. Используй delay().

3. Игнорирование отмены

Если корутина выполняет тяжёлую работу без точек приостановки (delay, await, withContext), её невозможно отменить. Вставляй yield() или ensureActive() в циклы.

4. Утечка памяти

Если ты запустил корутину в Activity (Android) и не отменил её при уничтожении Activity, она продолжит работать и держать ссылку на Activity. Используй viewModelScope или lifecycleScope.

5. Состояние гонки

Две корутины меняют одну переменную. Используй Mutex (мьютекс) или атомарные типы (AtomicInteger).

6. Слишком много корутин

Хотя корутины лёгкие, 1 000 000 корутин — это всё равно много памяти. Используй ограничение параллелизма (Semaphore или limitedParallelism).

Лучшие практики и советы профессионалов

  1. Всегда используй структурированную конкурентность. Не запускай глобальные корутины (GlobalScope.launch). Всегда привязывай корутины к жизненному циклу (Activity, ViewModel, скоуп запроса).
  2. Не смешивай блокирующий и неблокирующий код. Если нужно вызвать блокирующую библиотеку (старый JDBC, File API), оберни её в withContext(Dispatchers.IO).
  3. Используй async только когда нужен результат. Если результат не нужен — используй launch. async без await — это потенциальная утечка.
  4. Обрабатывай ошибки в корутинах. Используй try-catch внутри корутины или CoroutineExceptionHandler для глобальной обработки.
  5. Тестируй корутины. Используй runTest из библиотеки kotlinx-coroutines-test. Он позволяет управлять временем (ускорять delay).
  6. Изучи Flow. Flow — это асинхронная последовательность (как корутина, которая выдаёт несколько значений). Он идеален для реактивного программирования.

Вопросы для самопроверки

  1. Вопрос: Чем отличается launch от async?
    Ответ: launch запускает корутину, которая ничего не возвращает (Job). async возвращает Deferred, из которого можно получить результат через await().
  2. Вопрос: Что такое suspend-функция?
    Ответ: Функция, помеченная ключевым словом suspend. Она может приостанавливать своё выполнение, не блокируя поток, и возобновляться позже. Вызывать её можно только из корутины или другой suspend-функции.
  3. Вопрос: Что произойдёт, если вызвать Thread.sleep() внутри корутины?
    Ответ: Заблокируется весь поток, на котором выполняется корутина. Другие корутины в этом потоке не будут выполняться. Это плохая практика, нужно использовать delay().
  4. Вопрос: Как отменить корутину?
    Ответ: Вызвать job.cancel(). Корутина должна быть кооперативной — иметь точки приостановки (delay, yield, withContext), где она проверяет флаг отмены.
  5. Вопрос: Что такое Dispatcher и какие бывают?
    Ответ: Dispatcher определяет, на каких потоках выполняется корутина. Основные: Dispatchers.Default (CPU), Dispatchers.IO (ввод-вывод), Dispatchers.Main (UI).
  6. Вопрос: В чём разница между корутиной и потоком?
    Ответ: Поток — тяжёлый объект ОС (1 МБ стека). Корутина — лёгкий объект на уровне языка (несколько сотен байт). Корутины работают внутри потоков и могут приостанавливаться, освобождая поток для других задач.
  7. Вопрос: Что такое runBlocking и когда его использовать?
    Ответ: Функция, которая блокирует текущий поток до завершения всех корутин внутри. Используется только в main() и тестах. В реальных приложениях (Android, сервер) не используется.
  8. Вопрос: Как обработать исключение в корутине?
    Ответ: Обернуть код в try-catch внутри корутины. Или установить CoroutineExceptionHandler в контексте корутины.
  9. Вопрос: Что такое Channel?
    Ответ: Канал — это способ передачи данных между корутинами. Одна корутина отправляет (send), другая принимает (receive). Работает как потокобезопасная очередь.
  10. Вопрос: Чем Kotlin-корутины отличаются от Python asyncio?
    Ответ: Kotlin-корутины — часть языка, поддерживают структурированную конкурентность, многопоточность (Dispatchers.Default), легче и производительнее. Python asyncio — библиотека, однопоточная, требует ручного управления задачами.

Итог и что изучить дальше

Мы разобрали, что такое многопоточность и асинхронность, как они работают в Kotlin через корутины. Ты узнал:

  • Потоки — это тяжеловесные рабочие, корутины — лёгкие задачи.
  • Корутины приостанавливаются, а не блокируются.
  • Есть launch (запустить и забыть) и async (получить результат).
  • Диспетчеры управляют потоками.
  • Ошибки нужно обрабатывать, корутины — отменять.

Что изучить дальше:

  1. Flow — асинхронные последовательности данных (как корутина, которая выдаёт много значений). Это основа реактивного программирования в Kotlin.
  2. StateFlow и SharedFlow — для управления состоянием в UI.
  3. Room + Coroutines — как работать с базой данных асинхронно.
  4. Ktor — асинхронный HTTP-клиент и сервер на корутинах.
  5. Тестирование корутин — библиотека kotlinx-coroutines-test.
  6. Акторы (Actors) — модель для безопасной работы с состоянием в конкурентной среде.
Золотое правило: Асинхронность — это не про скорость, а про эффективность. Твоя программа не должна простаивать, ожидая ресурсы. Корутины в Kotlin делают это просто и безопасно.