Отладка корутин
Отладка приложений, использующих корутины, может быть непростой задачей: несколько корутин могут выполняться одновременно, приостанавливаться в одном потоке и возобновляться в другом. Порядок их выполнения и используемые ими потоки также могут меняться от запуска к запуску, из-за чего трудно отслеживать выполнение конкретной корутины.
На JVM можно использовать следующие возможности, чтобы упростить отладку корутин:
Режим отладки присваивает каждой корутине уникальное имя, чтобы её можно было определить в отладчике и диагностическом выводе.
Восстановление трассировки стека добавляет информацию о том, где корутина получает исключение вместо ожидаемого результата.
Агент отладки отслеживает активные корутины, сообщает об их состоянии и выполняет другие действия.
Режим отладки и восстановление трассировки стека доступны в модуле kotlinx-coroutines-core. Агент отладки доступен в модуле kotlinx-coroutines-debug.
Включение режима отладки
Режим отладки присваивает уникальное имя каждой запущенной корутине. Имена корутин можно увидеть в отладчике Java, в строковом представлении корутины и в имени потока, пока он выполняет корутину. Накладные расходы режима отладки во время выполнения незначительны, поэтому его можно оставить включённым, чтобы упростить ведение журналов и диагностику.
При запуске кода с включёнными утверждениями Java библиотека kotlinx.coroutines автоматически включает режим отладки. Модульные тесты по умолчанию запускаются с включёнными утверждениями, поэтому для них не нужно включать режим отладки отдельно.
Чтобы явно включить режим отладки, настройте инструмент сборки, например Gradle или Maven, либо конфигурацию запуска IDE так, чтобы аргумент -Dkotlinx.coroutines.debug передавался JVM, в которой выполняется приложение.
Чтобы включить режим отладки в IntelliJ IDEA, выполните следующие действия:
-
В виджете Run выберите конфигурацию запуска/отладки, которую хотите изменить, затем выберите Другие действия | Изменить:
-
В диалоговом окне Конфигурации запуска/отладки введите
-Dkotlinx.coroutines.debugв поле Параметры виртуальной машины и нажмите ОК:
Восстановление трассировки стека
Когда корутина получает исключение из другой корутины через приостанавливающую функцию, например Deferred.await(), трассировка стека исключения не содержит кадров стека принимающей корутины. Без этих кадров стека трассировка не показывает, где вызывается Deferred.await() и какие функции приводят к этому вызову, что может затруднить отладку.
Библиотека kotlinx.coroutines добавляет эту информацию с помощью восстановления трассировки стека, создавая копию исключения с дополнительными кадрами стека.
При возобновлении принимающая корутина выбрасывает копию, а не исходное исключение. Исходное исключение становится причиной копии. Если у исходного исключения есть подавленные исключения, они остаются связанными с ним, а не копируются. Сохранение их связи с исходным исключением помогает предотвратить циклы в цепочке исключений и сбои в некоторых фреймворках.
В режиме отладки восстановление трассировки стека включено по умолчанию. Чтобы отключить восстановление трассировки стека в режиме отладки, передайте параметр VM -Dkotlinx.coroutines.stacktrace.recovery=false.
Ниже приведён пример, показывающий разницу между трассировками стека с включённым и отключённым восстановлением трассировки стека:
import kotlinx.coroutines.*
object UserProfileService :
CoroutineScope by CoroutineScope(CoroutineName("UserProfileService")) {
private fun parseUserProfile(): String {
error("Invalid user profile")
}
private fun loadUserProfile(): String {
return parseUserProfile()
}
// Runs in the coroutine that calls this function
suspend fun awaitUserProfile() {
// Starts a new coroutine
val userProfile = async(Dispatchers.Default) {
// The new coroutine throws the exception
loadUserProfile()
}
// The coroutine running awaitUserProfile()
// receives the exception through the await() function
userProfile.await()
}
}
suspend fun main() {
UserProfileService.awaitUserProfile()
}
В этом примере функция parseUserProfile() выбрасывает исключение в корутине, запущенной функцией-билдером .async(). Корутина, вызывающая awaitUserProfile(), получает исключение через функцию Deferred.await().
При отключённом восстановлении трассировки стека трассировка показывает, где функция parseUserProfile() выбрасывает исключение в корутине, созданной функцией .async(), но не содержит вызов Deferred.await() в функции awaitUserProfile():

При включённом восстановлении трассировки стека трассировка также содержит вызов Deferred.await() в функции awaitUserProfile():

Восстановление трассировки стека для пользовательских исключений
Восстановление трассировки стека может автоматически копировать исключение, если его класс имеет открытый конструктор, принимающий сообщение, причину, оба аргумента или вовсе не принимающий аргументов.
Если вы хотите, чтобы библиотека kotlinx.coroutines восстанавливала трассировку стека исключения, для которого требуются дополнительные аргументы конструктора, например номер строки или код ошибки, реализуйте интерфейс StackTraceRecoverable.
Интерфейс StackTraceRecoverable входит в стандартную библиотеку Kotlin, поэтому его можно реализовать, не добавляя зависимость от библиотеки kotlinx.coroutines.
Чтобы реализовать интерфейс, переопределите функцию copyForStackTraceRecovery(). В переопределённой функции верните новый экземпляр исключения для восстановления трассировки стека или null, если не хотите, чтобы библиотека kotlinx.coroutines копировала исключение.
Эти API являются экспериментальными и требуют явного согласия с помощью аннотации @OptIn(ExperimentalStdlibCoroutineSupportApi::class).
Ниже приведён пример пользовательского исключения, которое сохраняет свойство line при создании нового экземпляра для восстановления трассировки стека:
import kotlinx.coroutines.*
import kotlin.coroutines.ExperimentalStdlibCoroutineSupportApi
import kotlin.coroutines.debug.StackTraceRecoverable
@OptIn(ExperimentalStdlibCoroutineSupportApi::class)
class FileEditException
// The implementation requires a private constructor
// to pass the cause to the IllegalStateException constructor
private constructor(
val line: Int,
private val detail: String,
cause: Throwable?,
) : IllegalStateException("When editing line $line: $detail", cause),
// Implements StackTraceRecoverable for stack trace recovery
StackTraceRecoverable<FileEditException> {
constructor(line: Int, detail: String) : this(line, detail, null)
// Copies the line number and message details
override fun copyForStackTraceRecovery(): FileEditException =
FileEditException(line, detail, this)
}
private fun editFile() {
throw FileEditException(15, "Unexpected token")
}
suspend fun main() {
supervisorScope {
// Starts a new coroutine
val fileEdit = async(Dispatchers.Default) {
// Throws the original exception
editFile()
}
// Stack trace recovery creates a copy of the exception,
// adds the calling coroutine's stack frames, and throws the copy
fileEdit.await()
}
}
При включённом режиме отладки вывод содержит восстановленную копию, за которой следует исходное исключение в качестве причины.
Агент отладки
Модуль kotlinx-coroutines-debug предоставляет агент отладки для приложений JVM. Агент отслеживает создание, приостановку и возобновление корутин.
API DebugProbes служит основной точкой входа для агента отладки. С его помощью можно выводить активные корутины и их текущее состояние. Вывод содержит трассировки стека, показывающие, где была создана каждая корутина и где она приостановлена. Также с его помощью можно вывести дамп корутин для иерархии конкретного Job или CoroutineScope.
Если включить DebugProbes в рабочей среде, производительность приложения может значительно снизиться, поскольку для каждой новой корутины будет создаваться трассировка стека. Чтобы избежать этих накладных расходов, установите для DebugProbes.enableCreationStackTraces значение false.
Добавление зависимости агента отладки
Чтобы использовать агент отладки в проекте, добавьте зависимость kotlinx-coroutines-debug:
// build.gradle.kts
dependencies {
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-debug:1.11.0")
}
<!-- pom.xml -->
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-debug</artifactId>
<version>1.11.0</version>
<scope>test</scope>
</dependency>
Отслеживание корутин с помощью агента отладки
Чтобы начать отслеживать корутины с помощью агента отладки, можно:
Добавить
-javaagent:/path/to/kotlinx-coroutines-debug-1.11.0.jarк параметрам VM, чтобы загрузить агент отладки при запуске приложения.Вызвать функцию
DebugProbes.install()перед запуском корутин, которые нужно отслеживать.
Когда агент отладки активен, можно использовать следующие API:
DebugProbes.dumpCoroutines()выводит все активные корутины.DebugProbes.dumpCoroutinesInfo()возвращает информацию об активных корутинах.DebugProbes.printJob()выводит дамп корутин для иерархииJob.DebugProbes.printScope()выводит дамп корутин для иерархииCoroutineScope.
Ниже приведён пример использования агента отладки для вывода активных корутин и иерархии корутин для конкретного Job:
import kotlinx.coroutines.*
import kotlinx.coroutines.debug.*
import kotlin.time.Duration.Companion.seconds
private suspend fun loadAccount() {
delay(5.seconds)
}
private suspend fun loadPreferences() {
delay(5.seconds)
}
private suspend fun loadUserProfile() = coroutineScope {
launch { loadAccount() }
launch { loadPreferences() }
}
@OptIn(ExperimentalCoroutinesApi::class)
fun main() {
// Installs the debug agent
// This is only required if you don't use the -javaagent VM option
DebugProbes.install()
runBlocking {
// Starts a coroutine with two child coroutines
val loadingJob = launch {
loadUserProfile()
}
// Gives the child coroutines time to suspend
delay(1.seconds)
// Prints all active coroutines
DebugProbes.dumpCoroutines()
println("============")
// Prints the loading job and its child coroutines
DebugProbes.printJob(loadingJob)
}
}
Если включён режим отладки, пример выведет следующие данные:
Вывод активных корутин при превышении времени ожидания тестов JUnit
Для тестов JUnit можно задать время ожидания с помощью соответствующего API CoroutinesTimeout, в зависимости от версии JUnit. API автоматически устанавливает диагностические пробы. Если тест не завершится до истечения времени ожидания, он выведет все активные корутины и их трассировки стека, а затем завершится с ошибкой.
JUnit 4
Чтобы задать время ожидания для тестов JUnit 4 и вывести все активные корутины и их трассировки стека при его превышении, используйте правило CoroutinesTimeout:
import kotlinx.coroutines.*
import kotlinx.coroutines.debug.junit4.CoroutinesTimeout
import org.junit.Rule
import org.junit.Test
import kotlin.time.Duration
@OptIn(ExperimentalCoroutinesApi::class)
class UserProfileTest {
@get:Rule
val timeout = CoroutinesTimeout.seconds(1)
private suspend fun loadUserProfile() {
withContext(Dispatchers.IO) {
// Simulates an operation that doesn't complete
delay(Duration.INFINITE)
}
}
@Test
fun loadsUserProfile() = runBlocking {
val loadingJob = launch {
loadUserProfile()
}
// Waits for the coroutine, so the test doesn't complete
loadingJob.join()
}
}
Через одну секунду правило сообщает, что время ожидания теста истекло, и выводит все активные корутины и их трассировки стека. Затем тест завершается с ошибкой TestTimedOutException.
JUnit 5
Чтобы задать время ожидания для всех тестовых функций класса, добавьте к классу аннотацию @CoroutinesTimeout:
import kotlinx.coroutines.*
import kotlinx.coroutines.debug.junit5.CoroutinesTimeout
import org.junit.jupiter.api.Test
import kotlin.time.Duration
@OptIn(ExperimentalCoroutinesApi::class)
// Sets a one-second timeout for all test functions in the class
@CoroutinesTimeout(testTimeoutMs = 1_000)
class UserProfileTest {
private suspend fun loadUserProfile() {
withContext(Dispatchers.IO) {
// Simulates an operation that doesn't complete
delay(Duration.INFINITE)
}
}
@Test
fun loadsUserProfile() = runBlocking {
val loadingJob = launch {
loadUserProfile()
}
// Waits for the coroutine, so the test doesn't complete
loadingJob.join()
}
}
Через одну секунду API CoroutinesTimeout сообщает об истечении времени ожидания и выводит все активные корутины и их трассировки стека. Затем тест завершается с ошибкой CoroutinesTimeoutException.
Устранение конфликтов ресурсов kotlinx-coroutines-debug на Android
Агент отладки не поддерживается на Android.
Модуль kotlinx-coroutines-debug имеет транзитивные зависимости от JNA, JNA Platform, Byte Buddy и Byte Buddy Agent. Некоторые из этих зависимостей содержат ресурсы с одинаковыми путями. При объединении Android ресурсов зависимостей дублирующиеся пути могут вызвать DuplicateRelativeFileException и привести к сбою сборки.
Чтобы устранить сбой сборки и сохранить зависимость kotlinx-coroutines-debug, исключите конфликтующие ресурсы с помощью следующей конфигурации packaging в файле build.gradle.kts:
// build.gradle.kts
android {
packaging {
resources {
// Excludes license files from JNA and JNA Platform
excludes += setOf(
"META-INF/AL2.0",
"META-INF/LGPL2.1",
)
// Excludes the ASM license file from Byte Buddy
excludes += "META-INF/licenses/ASM"
// Retains one copy of each Byte Buddy Agent file
pickFirsts += setOf(
"win32-x86-64/attach_hotspot_windows.dll",
"win32-x86/attach_hotspot_windows.dll",
)
}
}
}
Что дальше
Узнайте, как отлаживать корутины в IntelliJ IDEA, в руководствах Отладка корутин с помощью IntelliJ IDEA и Отладка Kotlin Flow с помощью IntelliJ IDEA.
© 2010–2026 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/coroutines-debugging.html