Spec-Zone.ru › Kotlin 1.6

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

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

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

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

Сопрограммы

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

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

Для Kotlin 1.6.20 рекомендуемая версия сопрограмм — 1.6.0.

Вы можете приостановить выполнение и выполнить работу в других потоках, используя другой механизм для планирования и управления этой работой. Однако эта версия 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.6.20 — 1.6.0-native-mt.

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

commonMain {
    dependencies {
        implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0-native-mt"
    }
}

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

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

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

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

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

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

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

CoroutineWorker

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

Для простых функций suspend это довольно хороший вариант, но он не поддерживает 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:

commonMain {
    dependencies {
        implementation "co.touchlab:stately-common:1.0.x"
    }
}

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

Последнее изменение: 07 апреля 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