Тема 1. Как выглядит Kotlin Coroutine без макияжа
Тема 2. Kotlin suspend функции
Код Kotlin корутин выполняется в потоках, но важно понимать, что корутины не привязаны жестко к конкретным потокам. Их выполнение управляется диспетчерами (Dispatchers), которые определяют, в каком потоке или пуле потоков будет работать корутина.
Как мы обсуждали в Как выглядит Kotlin Coroutine без макияжа при создании корутины создается объект Continuation, в котором содержится код, который выполняет корутина. Код делится на блоки - suspend-функции с помощью switch. Когда код доходит до suspend-функции она вызывается и в неё передается весь текущий объект Continuation и на этом это ответвление switch заканчивается. Suspend-функция по завершению своей работы вызывает метод resume у переданного ей объекта Continuation и таким образом начинается выполнение следующего ответвления switch'a.
Мы всё это вспомнили не просто так:
объект Continuation, который мы обсуждаем на всех этапах передается в специальной обёртке DispatchedContinuation.
Именно на этой обертке и вызывается метод resume, который выделяет поток и вызывает resume у объекта Continuation.
Исходя из изложенного выше мы можем понять механизм смены потока - он может меняться при каждом вызове метода resume у объекта DispatchedContinuation.
// Упрощенный пример DispatchedContinuation
class DispatchedContinuation<T>(
private val dispatcher: CoroutineDispatcher,
private val continuation: Continuation<T>
) : Continuation<T> {
override val context: CoroutineContext = continuation.context
override fun resumeWith(result: Result<T>) {
// 1. Диспетчер решает, в каком потоке выполнить код
dispatcher.dispatch(context) {
// 2. В выделенном потоке вызываем resume у оригинального Continuation
continuation.resumeWith(result)
}
}
}Доступны следующие диспетчеры:
Dispatchers.Default
Если не передать корутине в контекст диспетчер, то она использует диспетчер по умолчанию. Этот диспетчер представляет собой пул потоков, количество которых равно количеству ядер процессора (включая виртуальные). Его целесообразно использовать для CPU-интенсивных задач (сортировка, вычисления). Не рекомендуется запускать на этом диспетчере IO операции из-за относительно небольшого количества пула потоков, т.к. это ограничит параллельное выполнение операций - операции будут ждать в очереди освобождения занятых потоков.
Dispatchers.IO
Его лимит на потоки равен 64 (или числу ядер процессора, если их больше 64). Этот диспетчер целесообразно использовать для выполнения IO операций (сетевые запросы, работа с БД, чтение/запись файлов и т.п.). Не рекомендуется запускать на этом диспетчере CPU-интенсивные задачи, чтобы не перегрузить CPU.
Main
Main диспетчер выполняет код корутины в основном потоке.
Executor
Можно создать диспетчер с помощью Executor:Executors.newSingleThreadExecutor().asCoroutineDispatcher().
Тогда все будет выполняться на одном потоке:
val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
val scope = CoroutineScope(Job())
// or val scope = CoroutineScope(dispatcher)
repeat(6) {
scope.launch(dispatcher) {
Log.e("TAAAAAAAG", "Coroutine N$it, start")
TimeUnit.MILLISECONDS.sleep(100)
Log.e("TAAAAAAAG", "Coroutine N$it, end")
}
}
08:53:19.304 TAAAAAAAG Coroutine N0, start
08:53:19.404 TAAAAAAAG Coroutine N0, end
08:53:19.405 TAAAAAAAG Coroutine N1, start
08:53:19.505 TAAAAAAAG Coroutine N1, end
08:53:19.506 TAAAAAAAG Coroutine N2, start
08:53:19.606 TAAAAAAAG Coroutine N2, end
08:53:19.606 TAAAAAAAG Coroutine N3, start
08:53:19.707 TAAAAAAAG Coroutine N3, end
08:53:19.707 TAAAAAAAG Coroutine N4, start
08:53:19.808 TAAAAAAAG Coroutine N4, end
08:53:19.808 TAAAAAAAG Coroutine N5, start
08:53:19.908 TAAAAAAAG Coroutine N5, endUnconfined
Запускает корутину в текущем потоке, но возобновляет в потоке, предоставленном suspend-функцией.
Смена потока внутри корутины с suspend fun <T> withContext
withContext — это suspend-функция, которая:
Временно меняет контекст выполнения (диспетчер) для блока кода.
Выполняет переданный блок в указанном диспетчере.
Возвращает результат выполнения блока.
Автоматически возвращается в исходный диспетчер.
launch(Dispatchers.Main) {
// 1. Начинаем в UI-потоке
showLoading()
// 2. Переключаемся на IO-поток для сетевого запроса
val data = withContext(Dispatchers.IO) {
fetchUserData() // Выполняется в IO-потоке
}
// 3. Автоматически возвращаемся в UI-поток
updateUI(data) // Выполняется в Main-потоке
}Как работает механизм переключения
Шаг 1: Приостановка корутины
Когда корутина доходит до withContext, она:
Сохраняет текущий контекст (диспетчер).
Приостанавливает выполнение текущего состояния корутины.
Освобождает текущий поток (если он не
Dispatchers.Unconfined).
Шаг 2: Создание нового Continuation
Под капотом создается новый DispatchedContinuation, который:
Содержит переданный блок кода.
Связан с новым диспетчером (например,
Dispatchers.IO).Имеет callback для возврата результата.
// Упрощённый код
internal fun withContext(context: CoroutineContext, block: suspend () -> T): T {
// 1. Получаем текущий контекст
val oldContext = currentCoroutineContext()
// 2. Создаем новую обертку Continuation
val newContinuation = DispatchedContinuation(
newDispatcher = context[ContinuationInterceptor] as CoroutineDispatcher,
continuation = object : Continuation<T> {
override val context: CoroutineContext = oldContext
// Этот код выполнится после завершения блока
override fun resumeWith(result: Result<T>) {
// Вернемся в исходный контекст
oldContext[ContinuationInterceptor]?.interceptContinuation { continuation ->
continuation.resumeWith(result)
} ?: resumeUndispatchedWith(result)
}
}
)
// 3. Диспетчер запускает блок в своем потоке
newContinuation.dispatcher.dispatch(block)
// 4. Приостанавливаем текущую корутину
return suspendCoroutine { /* ... */ }
}Шаг 3: Выполнение в новом потоке
Новый диспетчер:
Берет свободный поток из своего пула.
Выполняет переданный блок кода.
По завершении вызывает
resumeдля продолжения.
Шаг 4: Возврат в исходный поток
После выполнения блока:
Результат передается через цепочку
Continuation.Специальный механизм (
UndispatchedContextили аналогичный) определяет, нужно ли возвращаться в исходный диспетчер.Если исходный диспетчер отличается от текущего, задача ставится в очередь исходного диспетчера.
// Механизм возврата упрощённо
fun resumeInOriginalContext(result: Result<T>) {
val originalDispatcher = savedOriginalDispatcher
if (originalDispatcher != currentDispatcher) {
// Ставим задачу в очередь исходного диспетчера
originalDispatcher.dispatch {
// Продолжаем выполнение корутины
originalContinuation.resumeWith(result)
}
} else {
// Остаемся в том же потоке
originalContinuation.resumeWith(result)
}
}