Конкурентность и сопрограммы
При работе с мобильными платформами вам может потребоваться написать многопоточный код, выполняющийся параллельно. Для этого вы можете использовать стандартную библиотеку kotlinx.coroutines или её многопоточную версию и альтернативные решения.
Рассмотрите плюсы и минусы каждого решения и выберите наиболее подходящий вариант для вашей ситуации.
Узнайте больше о конкурентности, текущем подходе и будущих улучшениях.
Сопрограммы
Сопрограммы — это лёгкие потоки, которые позволяют писать асинхронный неблокирующий код. Kotlin предоставляет библиотеку kotlinx.coroutines с рядом высокоуровневых примитивов, поддерживающих сопрограммы.
Текущая версия kotlinx.coroutines, которая может использоваться для iOS, поддерживает использование только в одном потоке. Вы не можете отправлять работу в другие потоки, изменяя диспетчер.
Для Kotlin 1.8.0 рекомендуемая версия сопрограмм — 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 в производстве, учитывая её особенности.
Текущая версия для Kotlin 1.8.0 — 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 доступным в общем коде, вы можете создать ожидаемые и фактические реализации для 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.
© 2010–2023 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