Spec-Zone.ru › Kotlin 1.9

Контекст сопрограммы и диспетчеры

Сопрограммы всегда выполняются в некотором контексте, представленном значением типа CoroutineContext, определённом в стандартной библиотеке Kotlin.

Контекст сопрограммы — это набор различных элементов. Основными элементами являются Job сопрограммы, с которым мы уже сталкивались, и её диспетчер, который рассматривается в этом разделе.

Диспетчеры и потоки

Контекст сопрограммы включает диспетчер сопрограммы (см. CoroutineDispatcher), который определяет, какой поток или потоки использует соответствующая сопрограмма для своего выполнения. Диспетчер сопрограммы может ограничить выполнение сопрограммы определённым потоком, распределить его по пулу потоков или позволить ему выполняться неограниченно.

Все билдеры сопрограмм, такие как launch и async, принимают необязательный параметр CoroutineContext, который может использоваться для явного указания диспетчера для новой сопрограммы и других элементов контекста.

Попробуйте следующий пример:

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
//sampleStart
    launch { // context of the parent, main runBlocking coroutine
        println("main runBlocking      : I'm working in thread ${Thread.currentThread().name}")
    }
    launch(Dispatchers.Unconfined) { // not confined -- will work with main thread
        println("Unconfined            : I'm working in thread ${Thread.currentThread().name}")
    }
    launch(Dispatchers.Default) { // will get dispatched to DefaultDispatcher 
        println("Default               : I'm working in thread ${Thread.currentThread().name}")
    }
    launch(newSingleThreadContext("MyOwnThread")) { // will get its own new thread
        println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}")
    }
//sampleEnd    
}

Вы можете получить полный код здесь.

Он выводит следующий результат (возможно, в другом порядке):

Unconfined            : I'm working in thread main
Default               : I'm working in thread DefaultDispatcher-worker-1
newSingleThreadContext: I'm working in thread MyOwnThread
main runBlocking      : I'm working in thread main

Когда launch { ... } используется без параметров, она наследует контекст (и, следовательно, диспетчер) от CoroutineScope, из которого она запускается. В этом случае она наследует контекст основной runBlocking сопрограммы, которая выполняется в потоке main.

Dispatchers.Unconfined — это специальный диспетчер, который, как кажется, также выполняется в потоке main, но на самом деле это другой механизм, который объясняется позже.

По умолчанию используется диспетчер, если в области видимости явно не указан другой диспетчер. Он представлен Dispatchers.Default и использует общий фоновый пул потоков.

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

Неограниченный против ограниченного диспетчера

Диспетчер сопрограмм Dispatchers.Unconfined запускает сопрограмму в потоке вызывающей стороны, но только до первой точки приостановки. После приостановки сопрограмма возобновляется в потоке, полностью определяемом приостанавливающей функцией, которая была вызвана. Диспетчер неограниченного типа подходит для сопрограмм, которые не потребляют время процессора и не обновляют какие-либо общие данные (например, пользовательский интерфейс), ограниченные определённым потоком.

С другой стороны, диспетчер по умолчанию наследуется от внешнего CoroutineScope. В частности, диспетчер по умолчанию для сопрограммы runBlocking ограничен потоком вызывающей стороны, поэтому наследование имеет эффект ограничения выполнения этим потоком с предсказуемым планированием FIFO.

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
//sampleStart
    launch(Dispatchers.Unconfined) { // not confined -- will work with main thread
        println("Unconfined      : I'm working in thread ${Thread.currentThread().name}")
        delay(500)
        println("Unconfined      : After delay in thread ${Thread.currentThread().name}")
    }
    launch { // context of the parent, main runBlocking coroutine
        println("main runBlocking: I'm working in thread ${Thread.currentThread().name}")
        delay(1000)
        println("main runBlocking: After delay in thread ${Thread.currentThread().name}")
    }
//sampleEnd    
}

Вы можете получить полный код здесь.

Выводит результат:

Unconfined      : I'm working in thread main
main runBlocking: I'm working in thread main
Unconfined      : After delay in thread kotlinx.coroutines.DefaultExecutor
main runBlocking: After delay in thread main

Таким образом, сопрограмма с контекстом, унаследованным от runBlocking {...}, продолжает выполняться в потоке main, в то время как неограниченная возобновляется в фоновом исполняемом потоке, который использует функция delay.

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

Отладка сопрограмм и потоков

Отладка с помощью IDEA

Отладчик сопрограмм плагина Kotlin упрощает отладку сопрограмм в IntelliJ IDEA.

Отладка работает для версий 1.3.8 или более поздних версии kotlinx-coroutines-core.

Окно инструментов Отладка содержит вкладку Сопрограммы. В этой вкладке вы можете найти информацию о выполняемых и приостановленных сопрограммах. Сопрограммы сгруппированы по диспетчерам, на которых они выполняются.

Debugging coroutines

С помощью отладчика сопрограмм вы можете:

  • Проверить состояние каждой сопрограммы.

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

  • Просмотреть полный стек создания сопрограммы, а также стек вызовов внутри сопрограммы. Стек включает все кадры с значениями переменных, даже те, которые были бы потеряны во время стандартной отладки.

  • Получить полный отчёт, содержащий состояние каждой сопрограммы и её стек. Для этого щелкните правой кнопкой мыши внутри вкладки Сопрограммы и выберите Получить дамп сопрограмм.

Для начала отладки сопрограмм вам нужно просто установить точки останова и запустить приложение в режиме отладки.

Дополнительные сведения об отладке сопрограмм см. в учебнике.

Отладка с помощью ведения журнала

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

Запустите следующий код с опцией -Dkotlinx.coroutines.debug JVM:

import kotlinx.coroutines.*

fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")

fun main() = runBlocking<Unit> {
//sampleStart
    val a = async {
        log("I'm computing a piece of the answer")
        6
    }
    val b = async {
        log("I'm computing another piece of the answer")
        7
    }
    log("The answer is ${a.await() * b.await()}")
//sampleEnd    
}

Вы можете получить полный код здесь.

Есть три сопрограммы. Основная сопрограмма (#1) внутри runBlocking и две сопрограммы, вычисляющие отложенные значения a (#2) и b (#3). Все они выполняются в контексте runBlocking и ограничены основным потоком. Вывод этого кода:

[main @coroutine#2] I'm computing a piece of the answer
[main @coroutine#3] I'm computing another piece of the answer
[main @coroutine#1] The answer is 42

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

Режим отладки также включается, когда JVM запускается с опцией -ea. Дополнительные сведения об инструментах отладки см. в документации к свойству DEBUG_PROPERTY_NAME.

Переключение между потоками

Запустите следующий код с опцией JVM -Dkotlinx.coroutines.debug (см. отладку):

import kotlinx.coroutines.*

fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")

fun main() {
//sampleStart
    newSingleThreadContext("Ctx1").use { ctx1 ->
        newSingleThreadContext("Ctx2").use { ctx2 ->
            runBlocking(ctx1) {
                log("Started in ctx1")
                withContext(ctx2) {
                    log("Working in ctx2")
                }
                log("Back to ctx1")
            }
        }
    }
//sampleEnd    
}

Полный код можно найти здесь.

Он демонстрирует несколько новых техник. Одна из них — использование runBlocking со явно заданным контекстом, а другая — использование функции withContext для изменения контекста корутины, оставаясь при этом в той же корутине, как показано в выводе ниже:

[Ctx1 @coroutine#1] Started in ctx1
[Ctx2 @coroutine#1] Working in ctx2
[Ctx1 @coroutine#1] Back to ctx1

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

Корутина Job в контексте

Корутина Job является частью ее контекста и может быть извлечена из него с помощью выражения coroutineContext[Job]:

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
//sampleStart
    println("My job is ${coroutineContext[Job]}")
//sampleEnd    
}

Полный код можно найти здесь.

В режиме отладки (режим отладки) он выводит что-то вроде этого:

My job is "coroutine#1":BlockingCoroutine{Active}@6d311334

Обратите внимание, что isActive в CoroutineScope — это просто удобный сокращенный вариант для coroutineContext[Job]?.isActive == true.

Дочерние корутины

Когда корутина запускается в CoroutineScope другой корутины, она наследует ее контекст через CoroutineScope.coroutineContext, и Job новой корутины становится дочерним для задания родительской корутины. Если родительская корутина отменяется, все ее дочерние корутины также отменяются рекурсивно.

Однако это родительско-дочернее отношение может быть явно переопределено одним из двух способов:

  1. Если при запуске корутины явно указан другой scope (например, GlobalScope.launch), то она не наследует Job от родительского scope.

  2. Если в качестве контекста новой корутины передается другой объект Job (как показано в примере ниже), то он переопределяет Job родительского scope.

В обоих случаях запущенная корутина не привязана к scope, из которого она была запущена, и работает независимо.

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
//sampleStart
    // launch a coroutine to process some kind of incoming request
    val request = launch {
        // it spawns two other jobs
        launch(Job()) { 
            println("job1: I run in my own Job and execute independently!")
            delay(1000)
            println("job1: I am not affected by cancellation of the request")
        }
        // and the other inherits the parent context
        launch {
            delay(100)
            println("job2: I am a child of the request coroutine")
            delay(1000)
            println("job2: I will not execute this line if my parent request is cancelled")
        }
    }
    delay(500)
    request.cancel() // cancel processing of the request
    println("main: Who has survived request cancellation?")
    delay(1000) // delay the main thread for a second to see what happens
//sampleEnd
}

Полный код можно найти здесь.

Вывод этого кода:

job1: I run in my own Job and execute independently!
job2: I am a child of the request coroutine
main: Who has survived request cancellation?
job1: I am not affected by cancellation of the request

Ответственность родителей

Родительская корутина всегда ждет завершения всех своих дочерних корутин. Родительской корутине не нужно явно отслеживать все запускаемые ею дочерние корутины, и ей не нужно использовать Job.join для ожидания их завершения:

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
//sampleStart
    // launch a coroutine to process some kind of incoming request
    val request = launch {
        repeat(3) { i -> // launch a few children jobs
            launch  {
                delay((i + 1) * 200L) // variable delay 200ms, 400ms, 600ms
                println("Coroutine $i is done")
            }
        }
        println("request: I'm done and I don't explicitly join my children that are still active")
    }
    request.join() // wait for completion of the request, including all its children
    println("Now processing of the request is complete")
//sampleEnd
}

Полный код можно найти здесь.

Результат будет:

request: I'm done and I don't explicitly join my children that are still active
Coroutine 0 is done
Coroutine 1 is done
Coroutine 2 is done
Now processing of the request is complete

Именование корутин для отладки

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

Следующий пример демонстрирует эту концепцию:

import kotlinx.coroutines.*

fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")

fun main() = runBlocking(CoroutineName("main")) {
//sampleStart
    log("Started main coroutine")
    // run two background value computations
    val v1 = async(CoroutineName("v1coroutine")) {
        delay(500)
        log("Computing v1")
        252
    }
    val v2 = async(CoroutineName("v2coroutine")) {
        delay(1000)
        log("Computing v2")
        6
    }
    log("The answer for v1 / v2 = ${v1.await() / v2.await()}")
//sampleEnd    
}

Полный код можно найти здесь.

Вывод, который он создает с опцией JVM -Dkotlinx.coroutines.debug, похож на:

[main @main#1] Started main coroutine
[main @v1coroutine#2] Computing v1
[main @v2coroutine#3] Computing v2
[main @main#1] The answer for v1 / v2 = 42

Комбинирование элементов контекста

Иногда нам нужно определить несколько элементов для контекста корутины. Для этого мы можем использовать оператор +. Например, мы можем запустить корутину с явно заданным диспетчером и явно заданным именем одновременно:

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
//sampleStart
    launch(Dispatchers.Default + CoroutineName("test")) {
        println("I'm working in thread ${Thread.currentThread().name}")
    }
//sampleEnd    
}

Полный код можно найти здесь.

Вывод этого кода с опцией JVM -Dkotlinx.coroutines.debug:

I'm working in thread DefaultDispatcher-worker-1 @test#2

Область действия сопрограммы

Давайте объединим наши знания о контекстах, дочерних элементах и задачах. Предположим, что наше приложение имеет объект с жизненным циклом, но этот объект не является сопрограммой. Например, мы пишем приложение для Android и запускаем различные сопрограммы в контексте активности Android для выполнения асинхронных операций по извлечению и обновлению данных, выполнению анимаций и т. д. Все эти сопрограммы должны быть отменены при уничтожении активности, чтобы избежать утечек памяти. Конечно, мы можем вручную управлять контекстами и задачами, чтобы связать жизненные циклы активности и её сопрограмм, но kotlinx.coroutines предоставляет абстракцию, инкапсулирующую это: CoroutineScope. Вы уже знакомы с областью действия сопрограммы, так как все билдеры сопрограмм объявлены как расширения для неё.

Мы управляем жизненными циклами наших сопрограмм, создавая экземпляр CoroutineScope, связанный с жизненным циклом нашей активности. Экземпляр CoroutineScope может быть создан с помощью функций-фабрик CoroutineScope() или MainScope(). Первый создаёт область действия общего назначения, а второй создаёт область действия для приложений с пользовательским интерфейсом и использует Dispatchers.Main в качестве диспетчера по умолчанию:

class Activity {
    private val mainScope = MainScope()
    
    fun destroy() {
        mainScope.cancel()
    }
    // to be continued ...

Теперь мы можем запускать сопрограммы в рамках этой Activity с помощью определённого scope. Для демонстрации мы запускаем десять сопрограмм, которые задерживаются на разное время:

    // class Activity continues
    fun doSomething() {
        // launch ten coroutines for a demo, each working for a different time
        repeat(10) { i ->
            mainScope.launch {
                delay((i + 1) * 200L) // variable delay 200ms, 400ms, ... etc
                println("Coroutine $i is done")
            }
        }
    }
} // class Activity ends

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

import kotlinx.coroutines.*

class Activity {
    private val mainScope = CoroutineScope(Dispatchers.Default) // use Default for test purposes
    
    fun destroy() {
        mainScope.cancel()
    }

    fun doSomething() {
        // launch ten coroutines for a demo, each working for a different time
        repeat(10) { i ->
            mainScope.launch {
                delay((i + 1) * 200L) // variable delay 200ms, 400ms, ... etc
                println("Coroutine $i is done")
            }
        }
    }
} // class Activity ends

fun main() = runBlocking<Unit> {
//sampleStart
    val activity = Activity()
    activity.doSomething() // run test function
    println("Launched coroutines")
    delay(500L) // delay for half a second
    println("Destroying activity!")
    activity.destroy() // cancels all coroutines
    delay(1000) // visually confirm that they don't work
//sampleEnd    
}

Полный код вы можете найти здесь.

Вывод этого примера:

Launched coroutines
Coroutine 0 is done
Coroutine 1 is done
Destroying activity!

Как видите, только первые две сопрограммы выведут сообщение, а остальные будут отменены одним вызовом job.cancel() в Activity.destroy().

Обратите внимание, что Android имеет поддержку областей действия сопрограмм в сущностях со всеми жизненными циклами. Смотрите соответствующую документацию.

Данные, локальные для потока

Иногда удобно иметь возможность передавать данные, локальные для потока, сопрограммам или между ними. Однако, так как они не привязаны к какому-либо конкретному потоку, это, скорее всего, приведёт к избыточному коду, если делать это вручную.

Для ThreadLocal, функция-расширение asContextElement приходит на помощь. Она создаёт дополнительный элемент контекста, который сохраняет значение заданного ThreadLocal и восстанавливает его каждый раз, когда сопрограмма меняет свой контекст.

Продемонстрировать это легко:

import kotlinx.coroutines.*

val threadLocal = ThreadLocal<String?>() // declare thread-local variable

fun main() = runBlocking<Unit> {
//sampleStart
    threadLocal.set("main")
    println("Pre-main, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
    val job = launch(Dispatchers.Default + threadLocal.asContextElement(value = "launch")) {
        println("Launch start, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
        yield()
        println("After yield, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
    }
    job.join()
    println("Post-main, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
//sampleEnd    
}

Полный код вы можете найти здесь.

В этом примере мы запускаем новую сопрограмму в пуле фоновых потоков с помощью Dispatchers.Default, поэтому она работает в другом потоке, чем пул потоков, но всё равно имеет значение переменной, локальной для потока, которую мы указали с помощью threadLocal.asContextElement(value = "launch"), независимо от того, в каком потоке выполняется сопрограмма. Таким образом, вывод (с отладкой) выглядит следующим образом:

Pre-main, current thread: Thread[main @coroutine#1,5,main], thread local value: 'main'
Launch start, current thread: Thread[DefaultDispatcher-worker-1 @coroutine#2,5,main], thread local value: 'launch'
After yield, current thread: Thread[DefaultDispatcher-worker-2 @coroutine#2,5,main], thread local value: 'launch'
Post-main, current thread: Thread[main @coroutine#1,5,main], thread local value: 'main'

Легко забыть установить соответствующий элемент контекста. Тогда переменная, локальная для потока, к которой обращается сопрограмма, может иметь неожиданное значение, если поток, выполняющий сопрограмму, отличается. Чтобы избежать подобных ситуаций, рекомендуется использовать метод ensurePresent и аварийно завершаться при неправильном использовании.

ThreadLocal имеет поддержку первого класса и может использоваться с любыми примитивными kotlinx.coroutines. Однако у него есть одно ключевое ограничение: при изменении локальной переменной для потока новое значение не распространяется на вызывающую сопрограмму (потому что элемент контекста не может отслеживать все ThreadLocal), и обновлённое значение теряется при следующем приостановлении. Используйте withContext, чтобы обновить значение локальной переменной для потока в сопрограмме, см. asContextElement для получения дополнительных сведений.

В качестве альтернативы значение можно хранить в изменяемой коробке, как class Counter(var i: Int), которая, в свою очередь, хранится в локальной переменной для потока. Однако в этом случае вы полностью несёте ответственность за синхронизацию потенциально конкурентных изменений в переменной в этой изменяемой коробке.

Для расширенного использования, например, для интеграции с MDC для ведения журнала, контекстами транзакций или другими библиотеками, которые внутри используют локальные переменные для потока для передачи данных, ознакомьтесь с документацией интерфейса ThreadContextElement, который должен быть реализован.

Последнее изменение: 25 июля 2023 г.
Компоновка функций приостановки Асинхронный поток

© 2010–2023 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/coroutine-context-and-dispatchers.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API