Если вы пишете на Kotlin для Android или backend, вы наверняка слышали о корутинах — элегантном инструменте для асинхронного программирования. Но теория теорией, а настоящая сила раскрывается в практике. В этой статье мы разберем конкретные, рабочие примеры использования корутин — от базовых операций до сложных паттернов, которые помогут вам писать отзывчивый, эффективный и понятный код без головной боли с коллбэками.
Что такое корутины и зачем они нужны?
Корутины — это легковесные потоки (threads), которые позволяют приостанавливать и возобновлять выполнение кода без блокировки основного потока. В отличие от традиционных потоков, их можно создавать тысячи без ущерба для производительности. Основная цель — упрощение асинхронного кода, делая его похожим на последовательный.
Ключевая идея: Корутины — не про многопоточность в классическом понимании, а про структурированную конкурентность. Они управляются не ОС, а самим Kotlin (с помощью планировщика).
Базовые примеры: с чего начать
Пример 1: Простой запуск и задержка
Самый простой способ запустить корутину — использовать GlobalScope.launch (хотя в продакшене его используют редко).
import kotlinx.coroutines.*
fun main() {
println("Основной поток: старт")
GlobalScope.launch {
delay(1000L) // Неблокирующая задержка на 1 секунду
println("Корутина: Привет через 1 секунду!")
}
println("Основной поток: работаю дальше")
Thread.sleep(2000L) // Блокировка основного потока, чтобы корутина успела выполниться
}
Здесь delay приостанавливает корутину, но не поток. Основной поток продолжает работать.
Пример 2: Получение результата из корутины (async/await)
Для вычисления результата используйте async, который возвращает Deferred (аналог Future/Promise).
suspend fun fetchUserData(): String {
delay(1500L) // Имитация долгого сетевого запроса
return "Данные пользователя"
}
suspend fun fetchNews(): String {
delay(1000L)
return "Свежие новости"
}
fun main() = runBlocking {
val time = measureTimeMillis {
val userDeferred = async { fetchUserData() }
val newsDeferred = async { fetchNews() }
println("${userDeferred.await()} и ${newsDeferred.await()}")
}
println("Выполнено за $time мс") // ~1500 мс, а не 2500!
}
Оба запроса запускаются конкурентно, общее время — время самого долгого.
Практические сценарии использования
Сценарий 1: Загрузка данных с UI (Android)
На Android важно не блокировать главный поток. С корутинами это становится тривиально.
// В Activity или ViewModel
fun loadUserProfile(userId: String) {
viewModelScope.launch {
// Показываем индикатор загрузки на UI потоке
_loadingState.value = true
try {
// IO-операция в фоне
val profile = withContext(Dispatchers.IO) {
repository.fetchProfile(userId)
}
// Обновляем UI на главном потоке (Dispatchers.Main по умолчанию в Android)
_profileState.value = profile
} catch (e: Exception) {
_errorState.value = "Ошибка загрузки"
} finally {
_loadingState.value = false
}
}
}
Важно: Всегда используйте скоупы (viewModelScope, lifecycleScope) вместо GlobalScope в Android, чтобы автоматически отменять корутины при уничтожении компонента.
Сценарий 2: Параллельная обработка коллекций
Обработка большого списка данных с использованием всех ядер процессора.
suspend fun processImages(images: List): List = coroutineScope {
images.map { image ->
async(Dispatchers.Default) { // Используем пул для CPU-интенсивных задач
heavyImageProcessing(image)
}
}.awaitAll() // Ждем завершения всех задач
}
Сценарий 3: Таймауты и отмена операций
Корутины предоставляют встроенные механизмы для контроля времени выполнения.
fun fetchDataWithTimeout() = runBlocking {
try {
val result = withTimeout(3000L) { // Таймаут 3 секунды
networkRequest() // Долгая операция
}
println("Результат: $result")
} catch (e: TimeoutCancellationException) {
println("Запрос отменен по таймауту")
}
}
// Отмена по требованию
val job = launch {
repeat(1000) { i ->
delay(500L)
println("Работаю $i...")
if (!isActive) return@launch // Проверяем, не отменена ли корутина
}
}
delay(2000L)
job.cancelAndJoin() // Отменяем и ждем завершения
println("Работа отменена")
Продвинутые паттерны
Паттерн: Ограничение конкурентности (Semaphore)
Когда нужно ограничить количество одновременно выполняемых операций (например, сетевых запросов).
val semaphore = Semaphore(3) // Не более 3 одновременно
fun makeLimitedRequests(urls: List) = runBlocking {
urls.map { url ->
async {
semaphore.withPermit { // Захватываем разрешение
makeRequest(url)
}
}
}.awaitAll()
}
Паттерн: Retry при ошибках
Повторная попытка выполнения при неудачном запросе с экспоненциальной задержкой.
suspend fun retryWithBackoff(
times: Int = 3,
initialDelay: Long = 100,
maxDelay: Long = 1000,
block: suspend () -> T
): T {
var currentDelay = initialDelay
repeat(times - 1) { attempt ->
try {
return block()
} catch (e: Exception) {
println("Попытка ${attempt + 1} не удалась: ${e.message}")
}
delay(currentDelay)
currentDelay = (currentDelay * 2).coerceAtMost(maxDelay)
}
return block() // Последняя попытка
}
FAQ: Часто задаваемые вопросы
Чем корутины лучше RxJava или потоков?
Корутины проще в изучении и использовании, имеют менее крутую кривую обучения. Код выглядит как последовательный, что улучшает читаемость. Они легковеснее потоков и интегрированы в язык.
Нужно ли использовать Dispatchers всегда явно?
Да, это лучшая практика. Явное указание диспетчера (Dispatchers.IO для сетевых/дисковых операций, Dispatchers.Default для CPU-интенсивных задач, Dispatchers.Main для UI) делает код более предсказуемым и эффективным.
Как тестировать код с корутинами?
Используйте runTest из библиотеки kotlinx-coroutines-test. Она позволяет контролировать виртуальное время и избегать реальных задержек в тестах.
Что такое structured concurrency?
Это принцип, согласно которому корутины запускаются только в определенных скоупах, которые контролируют их жизненный цикл. Это предотвращает утечки и упрощает отмену операций.
Можно ли использовать корутины в Java проектах?
Да, но с ограничениями. Kotlin корутины можно вызывать из Java, но создавать и полноценно работать с ними удобнее только из Kotlin-кода.