Spec-Zone.ru › Kotlin 1.9

Конкурентная изменчивость

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

При работе с iOS, модель состояния и конкурентности Kotlin/Native имеет два простых правила.

  1. Изменяемое, не-замороженное состояние видно только одной потоку за раз.

  2. Неизменяемое, замороженное состояние может быть разделено между потоками.

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

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

Существует множество техник, которые помогают обойти эти ограничения, каждая со своими плюсами и минусами:

  • Атомарные операции

  • Потокоизолированные состояния

  • Возможности низкого уровня

Атомарные операции

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

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

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

AtomicInt/AtomicLong

Первые два — это простые числовые типы: AtomicInt и AtomicLong. Они позволяют иметь общее Int или Long, которое может быть читаемо и изменяемо из нескольких потоков.

object AtomicDataCounter {
    val count = AtomicInt(3)
  
    fun addOne() {
        count.increment()
    }
}

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

AtomicReference

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

data class SomeData(val i: Int)

object GlobalData {
    var sd = SomeData(0)

    fun storeNewValue(i: Int) {
        sd = SomeData(i) //Doesn't work
    }
}

Согласно правилам глобального состояния, глобальные object значения заморожены в Kotlin/Native, поэтому попытка изменить sd завершится ошибкой. Вы можете реализовать это с помощью AtomicReference:

data class SomeData(val i: Int)

object GlobalData {
    val sd = AtomicReference(SomeData(0).freeze())

    fun storeNewValue(i: Int) {
        sd.value = SomeData(i).freeze()
    }
}

Сам AtomicReference заморожен, что позволяет ему существовать внутри замороженного объекта. Данные внутри экземпляра AtomicReference явно заморожены в приведенном коде. Однако в многоплатформенных библиотеках данные будут замораживаться автоматически. Если вы используете AtomicReference среды выполнения Kotlin/Native, вы должны явно вызвать freeze().

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

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

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

  • Если у вас есть состояние, которое может иметь циклические ссылки и нуждается в освобождении, вы должны использовать необязательный тип в AtomicReference и явно установить его в null, когда вы закончите с ним.

  • Если вы храните AtomicReference в глобальном объекте, который никогда не выходит из области видимости, это не имеет значения (потому что память никогда не нужно освобождать во время работы процесса).

class Container(a:A) {
    val atom = AtomicReference<A?>(a.freeze())

    /**
     * Call when you're done with Container
     */
    fun clear(){
        atom.value = null
    }
}

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

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

object MyListCache {
    val atomicList = AtomicReference(listOf<String>().freeze())
    fun addEntry(s:String){
        val l = atomicList.value
        val newList = mutableListOf<String>()
        newList.addAll(l)
        if(!newList.contains(s)){
            newList.add(s)
        }
        atomicList.value = newList.freeze()
    }
}

Вам понадобится реализовать какую-либо форму блокировки или логику проверки и установки, чтобы обеспечить правильную конкурентность.

Потокоизолированные состояния

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

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

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

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

Thread-isolated state

Реализация потокоизолированных состояний несколько сложна, но существуют библиотеки, которые предоставляют эту функциональность.

AtomicReference против потокоизолированного состояния

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

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

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

END_OF_DOCUMENT_MARKER

Возможности низкого уровня

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

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

Kotlin/Native работает поверх C++ и обеспечивает взаимодействие с C и Objective-C. Если вы работаете на iOS, вы также можете передавать лямбда-аргументы в свой общий код из Swift. Весь этот нативный код выполняется вне ограничений состояния Kotlin/Native.

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

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

Одним из преимуществ платформенно-ориентированного подхода является производительность. С другой стороны, вам потребуется самостоятельно управлять конкурентностью. Objective-C не знает о frozen, но если вы сохраняете состояния из Kotlin в структурах Objective-C и используете их между потоками, состояния Kotlin обязательно должны быть заморожены. Среда выполнения Kotlin/Native, как правило, будет предупреждать вас о проблемах, но возможны проблемы с конкурентностью в коде низкого уровня, которые очень трудно отследить. Также очень легко создать утечки памяти.

Поскольку в приложении Kotlin Multiplatform вы также нацелены на JVM, вам понадобятся альтернативные способы реализации того, для чего вы используете код нативного уровня платформы. Это, очевидно, потребует больше работы и может привести к несовместимости между платформами.

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

Последнее изменение: 22 сентября 2022 г.
Обзор конкурентности Конкурентность и сопрограммы

© 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-concurrent-mutability.html

Spec-Zone.ru

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