Spec-Zone.ru › Kotlin 1.9

Обзор конкурентности

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

При расширении вашего опыта разработки с Android до Kotlin Multiplatform для мобильных устройств, вы столкнётесь с другой моделью состояния и конкурентности для iOS. Это модель Kotlin/Native, которая компилирует код Kotlin в нативные двоичные файлы, которые могут выполняться без виртуальной машины, например, на iOS.

Доступ к изменяемой памяти для нескольких потоков одновременно, если это не ограничено, известно как рискованный и подверженный ошибкам. Такие языки, как Java, C++ и Swift/Objective-C, позволяют нескольким потокам получать доступ к одному состоянию без ограничений. Проблемы с конкурентностью отличаются от других проблем программирования тем, что их часто очень сложно воспроизвести. Вы можете не увидеть их во время разработки локально, и они могут возникать спорадически. Иногда вы можете увидеть их только в производстве при большой нагрузке.

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

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

Правила совместного использования состояния

Kotlin/Native вводит правила для совместного использования состояний между потоками. Эти правила существуют для предотвращения небезопасного совместного доступа к изменяемым состояниям. Если вы приходите из среды JVM и пишете конкурентный код, вам может потребоваться изменить архитектуру ваших данных, но это позволит вам достичь тех же результатов без рискованных побочных эффектов.

Также важно отметить, что есть способы работы с этими правилами. Цель состоит в том, чтобы обход этих правил был необходимостью редко, если вообще никогда.

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

Правило 1: Изменяемое состояние == 1 поток

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

data class SomeData(var count:Int)

fun simpleState(){
    val sd = SomeData(42)
    sd.count++
    println("My count is ${sd.count}") // It will be 43
}

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

Правило 2: Неизменяемое состояние == много потоков

Если состояние не может быть изменено, несколько потоков могут безопасно к нему обращаться. В Kotlin/Native неизменяемое не означает, что всё является val. Это означает замороженное состояние.

Неизменяемое и замороженное состояние

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

data class SomeData(val s:String, val i:Int)

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

data class SomeData(val s:String, val i:SomeInterface)

Kotlin/Native необходимо проверить, что какая-то часть состояния действительно неизменяема во время выполнения. Система выполнения может просто пройтись по всему состоянию и проверить, что каждая часть является глубоко неизменяемой, но это будет негибко. И если вам нужно это делать каждый раз, когда система выполнения хочет проверить изменяемость, это будет иметь значительные последствия для производительности.

Kotlin/Native определяет новое состояние выполнения, называемое замороженным. Любой экземпляр объекта может быть заморожен. Если объект заморожен:

  1. Вы не можете изменить ни одну часть его состояния. Попытка сделать это приведёт к исключению во время выполнения: InvalidMutabilityException. Замороженный экземпляр объекта является 100%-ной, проверенной во время выполнения, неизменяемой.

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

Система выполнения Native добавляет функцию расширения freeze() ко всем классам. Вызов freeze() заморозит объект и всё, на что ссылается объект, рекурсивно.

data class MoreData(val strData: String, var width: Float)
data class SomeData(val moreData: MoreData, var count: Int)
//...
val sd = SomeData(MoreData("abc", 10.0), 0)
sd.freeze()
Freezing state
  • freeze() — это одностороннее действие. Вы не можете разморозить что-либо.

  • freeze() недоступен в общем коде Kotlin, но несколько библиотек предоставляют expect и actual объявления для использования в общем коде. Однако, если вы используете библиотеку конкурентности, такую как kotlinx.coroutines, она, скорее всего, автоматически заморозит данные, пересекающие границы потоков.

freeze не уникально для Kotlin. Вы также можете найти его в Ruby и JavaScript.

Глобальное состояние

Kotlin позволяет определять состояние как доступное глобально. Если это просто изменяемое состояние, глобальное состояние нарушает правило 1. Для соответствия правилам состояния Kotlin/Native глобальное состояние имеет некоторые особые условия. Эти условия замораживают состояние или делают его видимым только для одного потока.

Глобальный объект

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

object SomeState{
    var count = 0
    fun add(){
        count++ //This will throw an exception
    }
}

Попытка изменить count вызовет исключение, потому что SomeState заморожен (что означает, что все его данные заморожены).

Вы можете сделать глобальный объект потоколокальным, что позволит ему быть изменяемым и предоставит каждому потоку копию его состояния. Отметьте его аннотацией @ThreadLocal.

@ThreadLocal
object SomeState{
    var count = 0
    fun add(){
        count++ //👍
    }
}

Если разные потоки читают count, они получат разные значения, потому что у каждого потока своя копия.

Эти правила для глобальных объектов также применяются к объектам-компаньонам.

class SomeState{
    companion object{
        var count = 0
        fun add(){
            count++ //This will throw an exception
        }
    }
}

Глобальные свойства

Глобальные свойства являются особым случаем. Они доступны только главному потоку, но они изменяемы. Доступ к ним из других потоков вызовет исключение.

val hello = "Hello" //Only main thread can see this

Вы можете добавить к ним аннотации:

  • @SharedImmutable, что сделает их глобально доступными, но замороженными.

  • @ThreadLocal, что даст каждому потоку свою изменяемую копию.

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

Текущие и будущие модели

Правила конкурентности Kotlin/Native потребуют корректировки архитектурного дизайна, но с помощью библиотек и новых лучших практик, повседневная разработка практически не изменится. На самом деле, соблюдение правил Kotlin/Native, касающихся кроссплатформенного кода, приведет к более безопасной конкурентности в мобильных приложениях.

В приложении Kotlin Multiplatform у вас есть Android и iOS цели с различными правилами состояния. Некоторые команды, как правило, работающие с большими приложениями, разделяют код для очень специфичных функций и часто управляют конкурентностью в платформенном приложении. Это потребует явного замораживания состояний, возвращаемых из Kotlin, но в остальном это просто.

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

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

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

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

Последнее изменение: 11 апреля 2023
Неизменяемость и конкурентность в Kotlin/Native Конкурентная изменяемость

© 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-overview.html

Spec-Zone.ru

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