Spec-Zone.ru › Kotlin 1.7

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

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

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

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

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

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

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

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

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

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

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

Если ваше состояние изменяемо, только один поток может видеть его в данный момент. Любое обычное состояние класса, которое вы обычно используете в Kotlin, считается изменяемым в runtime Kotlin/Native. Если вы не используете конкурентность, 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 должен проверить, что какая-то часть состояния действительно неизменяема во время выполнения. Runtime мог бы просто пройти по всему состоянию и проверить, что каждая часть является глубоко неизменяемой, но это было бы негибко. И если вам нужно делать это каждый раз, когда runtime хочет проверить изменяемость, это будет иметь серьезные последствия для производительности.

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

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

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

Runtime 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, что даст каждому потоку свою изменяемую копию.

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

END_OF_DOCUMENT_MARKER

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

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

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

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

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

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

Этот материал был подготовлен компанией 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-overview.html

Spec-Zone.ru

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