Spec-Zone.ru › Kotlin 1.7

Асинхронность и сопрограммы

Эта страница описывает возможности устаревшего менеджера памяти. Ознакомьтесь с управлением памятью в Kotlin/Native, чтобы узнать о новом менеджере памяти, который по умолчанию включен начиная с Kotlin 1.7.20.

При работе с мобильными платформами вам может потребоваться написать многопоточный код, выполняемый параллельно. Для этого вы можете использовать стандартную библиотеку kotlinx.coroutines или её многопоточную версию и альтернативные решения.

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

Узнайте больше о асинхронности, текущем подходе и будущих улучшениях.

Сопрограммы

Сопрограммы — это лёгкие потоки, которые позволяют писать асинхронный неблокирующий код. Kotlin предоставляет библиотеку kotlinx.coroutines с рядом высокоуровневых примитивов, поддерживающих сопрограммы.

Текущая версия kotlinx.coroutines, которую можно использовать для iOS, поддерживает использование только в одном потоке. Вы не можете отправлять работу в другие потоки, изменяя диспетчер.

Рекомендуемая версия сопрограмм для Kotlin 1.7.20 — 1.6.4.

Вы можете приостановить выполнение и выполнять работу в других потоках, используя другой механизм для планирования и управления этой работой. Однако эта версия kotlinx.coroutines не может самостоятельно менять потоки.

Также существует другая версия kotlinx.coroutines, которая поддерживает работу с несколькими потоками.

Ознакомьтесь с основными концепциями использования сопрограмм:

  • Асинхронность против параллельной обработки

  • Диспетчер для изменения потоков

  • Замороженные захваченные данные

  • Замороженные возвращаемые данные

Асинхронность против параллельной обработки

Асинхронная и параллельная обработка — разные вещи.

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

Следующий код выполняет сетевой вызов с помощью Ktor. В основном потоке вызов инициируется и приостанавливается, в то время как другой процесс выполняет фактические сетевые операции. По завершении код возобновляется в основном потоке.

val client = HttpClient()
//Running in the main thread, start a `get` call
client.get<String>("https://example.com/some/rest/call")
//The get call will suspend and let other work happen in the main thread, and resume when the get call completes

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

Диспетчер для изменения потоков

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

suspend fun differentThread() = withContext(Dispatchers.Default){
    println("Different thread")
}

withContext принимает как аргумент диспетчер и блок кода, который будет выполняться в потоке, определённом диспетчером. Узнайте больше о контексте и диспетчерах сопрограмм.

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

Замороженные захваченные данные

Чтобы запустить код в другом потоке, вы передаёте functionBlock, который замораживается и затем выполняется в другом потоке.

fun <R> runOnDifferentThread(functionBlock: () -> R)

Вы вызовете эту функцию следующим образом:

runOnDifferentThread {
    //Code run in another thread
}

Как описано в обзоре асинхронности, состояние, используемое в Kotlin/Native между потоками, должно быть заморожено. Аргумент функции — это состояние, которое будет заморожено вместе со всем, что оно захватывает.

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

В следующем примере экземпляр класса данных dc будет захвачен блоком функции и заморожен при пересечении потоков. Инструкция println выведет true.

val dc = DataClass("Hello")
withContext(Dispatchers.Default) {
    println("${dc.isFrozen}")
}

При выполнении параллельного кода будьте внимательны к захваченному состоянию. Иногда очевидно, когда состояние будет захвачено, но не всегда. Например:

class SomeModel(val id:IdRec){
    suspend fun saveData() = withContext(Dispatchers.Default){
        saveToDb(id)
    }
}

Код внутри saveData выполняется в другом потоке. Это заморозит id, но так как id — это свойство родительского класса, то заморозится и родительский класс.

Замороженные возвращаемые данные

Данные, возвращаемые из другого потока, также замораживаются. Хотя рекомендуется возвращать неизменяемые данные, вы можете вернуть изменяемое состояние таким образом, что возвращаемое значение нельзя будет изменить.

val dc = withContext(Dispatchers.Default) {
    DataClass("Hello Again")
}

println("${dc.isFrozen}")

Это может быть проблемой, если изменяемое состояние изолировано в одном потоке, а для связи используются операции потоков сопрограмм. Если вы попытаетесь вернуть данные, которые сохраняют ссылку на изменяемое состояние, данные также будут заморожены по ассоциации.

Узнайте больше о состоянии, изолированном в потоке.

Многопоточные сопрограммы

Ветвь kotlinx.coroutines библиотеки kotlinx.coroutines предоставляет поддержку использования нескольких потоков. Это отдельная ветвь по причинам, описанным в статье о будущем модели асинхронности.

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

Текущая версия для Kotlin 1.7.20 — 1.6.4-native-mt.

Чтобы использовать многопоточную версию, добавьте зависимость для набора commonMain в build.gradle(.kts):

val commonMain by getting {
    dependencies {
        implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4-native-mt")
    }
}
commonMain {
    dependencies {
        implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4-native-mt'
    }
}

При использовании других библиотек, которые также зависят от kotlinx.coroutines, например, Ktor, убедитесь, что вы указали многопоточную версию kotlinx-coroutines. Это можно сделать с помощью strictly:

implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4-native-mt") {
    version {
        strictly("1.6.4-native-mt")
    }
}
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4-native-mt' {
    version {
        strictly '1.6.4-native-mt'
    }
}

Поскольку основная версия kotlinx.coroutines — однопоточная, библиотеки, скорее всего, будут полагаться на эту версию. Если вы видите InvalidMutabilityException в связи с операцией сопрограммы, то, вероятно, вы используете не ту версию.

Использование многопоточных сопрограмм может привести к утечкам памяти. Это может быть проблемой для сложных сценариев сопрограмм под нагрузкой. Мы работаем над решением этой проблемы.

Посмотрите полный пример использования многопоточных сопрограмм в приложении Kotlin Multiplatform.

Альтернативы kotlinx-coroutines

Существует несколько альтернативных способов выполнения параллельного кода.

CoroutineWorker

CoroutinesWorker — это библиотека, опубликованная AutoDesk, которая реализует некоторые функции сопрограмм между потоками, используя однопоточную версию kotlinx.coroutines.

Для простых приостановленных функций это довольно хороший вариант, но он не поддерживает Flow и другие структуры.

Reaktive

Reaktive — это библиотека, похожая на Rx, которая реализует реактивные расширения для Kotlin Multiplatform. Она имеет некоторые расширения для сопрограмм, но в основном предназначена для RX и потоков.

Пользовательский процессор

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

Конкурентность платформы

В производстве вы также можете положиться на платформу для обработки конкурентности. Это может быть полезно, если общий код Kotlin будет использоваться для бизнес-логики или операций с данными, а не для архитектуры.

Для совместного использования состояния в iOS между потоками это состояние должно быть замороженным. Библиотеки конкурентности, упомянутые здесь, автоматически замораживают ваши данные. Вам редко потребуется делать это явно, если вообще когда-либо.

Если вы возвращаете данные на платформу iOS, которые должны быть совместно использованы между потоками, убедитесь, что данные заморожены до выхода за пределы iOS.

Kotlin имеет понятие замораживания только для платформ Kotlin/Native, включая iOS. Чтобы сделать freeze доступным в общем коде, вы можете создать реализации expect и actual для freeze, или использовать stately-common, которая предоставляет эту функциональность. В Kotlin/Native freeze заморозит ваше состояние, в то время как в JVM ничего не сделает.

Чтобы использовать stately-common, добавьте зависимость для набора исходных кодов commonMain в build.gradle(.kts):

val commonMain by getting {
    dependencies {
        implementation ("co.touchlab:stately-common:1.0.x")
    }
}
commonMain {
    dependencies {
        implementation 'co.touchlab:stately-common:1.0.x'
    }
}

Этот материал был подготовлен компанией Touchlab для публикации компанией JetBrains.

Последнее изменение: 22 сентября 2022 г.
Конкурентная изменчивость Отладка Kotlin/Native

© 2010–2022 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/multiplatform-mobile-concurrency-and-coroutines.html

Spec-Zone.ru

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