Подтвердить что ты не робот

Должен ли доступ к scala.collection.immutable.List и Vector быть синхронизированы?

Я прохожу через Обучение параллельному программированию в Scala и столкнулся с следующим:

В текущих версиях Scala, однако, некоторые коллекции, которые считающиеся неизменяемыми, такие как List и Vector, не могут использоваться совместно синхронизации. Хотя их внешний API не позволяет изменять их, они содержат нефинальные поля.

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

От обучения параллельному программированию в Scala от Aleksandar Prokopec, конец главы 2 (стр .58), Packt Publishing, ноябрь 2014 г.

Может ли это быть правильно?

Мое рабочее предположение всегда заключалось в том, что любая внутренняя изменчивость (для реализации лень, кеширования и т.д.) в структурах данных библиотеки Scala, описанных как неизменяемая, была бы идемпотентной, так что худшее, что может случиться в плохом расе, - это работа быть излишне дублированным. Этот автор, кажется, предполагает, что правильность может быть поставлена ​​под угрозу путем одновременного доступа к неизменяемым структурам. Это правда? Нам действительно нужно синхронизировать доступ к спискам?

Значительная часть моего перехода к неизменному стилю была вызвана стремлением избежать синхронизации и потенциальными соперничающими над ними проблемами. Было бы несчастливым серьезным делом узнать, что синхронизацию нельзя отбросить для Scala основных "неизменных" структур данных. Этот автор просто чрезмерно консервативен?

Scala документация коллекций включает в себя следующее:

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

Это не совсем означает, что они безопасны для одновременного доступа несколькими потоками. Кто-нибудь знает об авторитетном утверждении, что они (или нет)?

4b9b3361

Ответ 1

Это зависит от того, где вы делитесь ими:

  • Небезопасно делиться ими внутри scala -library
  • Небезопасно делиться ими с Java-кодом, отражением

Проще говоря, эти коллекции менее защищены, чем объекты с только конечными полями. Независимо от того, что они одинаковы на уровне JVM (без оптимизации, например ldc) - оба могут быть полями с некоторым изменяемым адресом, поэтому вы можете изменить их с помощью команды putfield bytecode. В любом случае, var по-прежнему менее защищен компилятором в сравнении с java final, scala final val и val.

Тем не менее, в большинстве случаев все равно использовать их, поскольку их поведение логически неизменено - все изменяемые операции инкапсулированы (для scala -code). Давайте посмотрим на Vector. Для реализации алгоритма добавления требуется изменяемые поля:

private var dirty = false

//from VectorPointer
private[immutable] var depth: Int = _
private[immutable] var display0: Array[AnyRef] = _
private[immutable] var display1: Array[AnyRef] = _
private[immutable] var display2: Array[AnyRef] = _
private[immutable] var display3: Array[AnyRef] = _
private[immutable] var display4: Array[AnyRef] = _
private[immutable] var display5: Array[AnyRef] = _

который реализуется следующим образом:

val s = new Vector(startIndex, endIndex + 1, blockIndex)
s.initFrom(this) //uses displayN and depth
s.gotoPos(startIndex, startIndex ^ focus) //uses displayN
s.gotoPosWritable //uses dirty
...
s.dirty = dirty

И s приходит к пользователю только после того, как метод вернул его. Таким образом, он даже не заботится о гарантиях happens-before - все измененные операции выполняются в одном потоке (поток, где вы вызываете :+, +: или updated)), это просто своего рода инициализация. Единственная проблема здесь в том, что private[somePackage] доступен непосредственно из Java-кода и из scala -library, поэтому, если вы передадите его некоторые Java-методы могли бы изменить их.

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

final case class ::[B](override val head: B, private[scala] var tl: List[B]) extends List[B] {
  override def tail : List[B] = tl
  override def isEmpty: Boolean = false
}

Но они использовали только внутри методов библиотеки (внутри одного потока) без какого-либо явного совместного использования или создания потоков, и они всегда возвращают новую коллекцию, рассмотрим take в качестве примера:

override def take(n: Int): List[A] = if (isEmpty || n <= 0) Nil else {

    val h = new ::(head, Nil)
    var t = h
    var rest = tail
    var i = 1
    while ({if (rest.isEmpty) return this; i < n}) {
      i += 1
      val nx = new ::(rest.head, Nil)
      t.tl = nx //here is mutation of t filed 
      t = nx
      rest = rest.tail
    }
    h
}

Итак, здесь t.tl = nx не сильно отличается от t = nx по смыслу безопасности потоков. Оба они отображаются только из одного стека (стек take). Если я добавлю, скажем someActor ! t (или любую другую операцию async), someField = t или someFunctionWithExternalSideEffect(t) прямо внутри цикла while - я мог бы разорвать этот контракт.


Немного добавлено здесь об отношениях с JSR-133:

1) new ::(head, Nil) создает новый объект в куче и помещает его адрес (допустим, 0x100500) в стек (val h =)

2), пока этот адрес находится в стеке, он известен только текущему потоку

3) Другие потоки могут быть задействованы только после совместного использования этого адреса, помещая его в какое-то поле; в случае take перед тем, как вызвать areturn (return h), он должен очистить любые кеши (для восстановления стека и регистров), поэтому возвращаемый объект будет согласован.

Таким образом, все операции над объектом 0x100500 выходят за рамки JSR-133, если 0x100500 является частью только стека (а не кучей, а не других стеков). Однако некоторые поля объекта 0x100500 могут указывать на некоторые общие объекты (которые могут быть в области JSR-133), но здесь это не так (поскольку эти объекты неизменяемы для внешних).


Я думаю, что (надежда) автор имел в виду логические гарантии синхронизации для разработчиков библиотек - вам все равно нужно быть осторожным с этими вещами, если вы разрабатываете scala -library, поскольку эти var являются private[scala], private[immutable] так что, возможно написать некоторый код, чтобы мутировать их из разных потоков. С точки зрения разработчика scala -library это обычно означает, что все мутации в одном экземпляре должны применяться в одном потоке и только в коллекции, невидимой для пользователя (на данный момент). Или просто говоря: не открывайте изменяемые поля для внешних пользователей.

P.S. scala имел несколько неожиданных проблем с синхронизацией, что вызвало непредсказуемость частей библиотеки, поэтому я бы не стал если что-то может быть неправильным (и это ошибка тогда), но, допустим, 99% случаев для 99% методов неизменяемых коллекций являются потокобезопасными. В худшем случае вам может быть отказано в использовании какого-либо сломанного метода или просто (это может быть не просто "просто" для некоторых случаев), нужно клонировать коллекцию для каждого потока.

В любом случае, неизменность по-прежнему является хорошим способом обеспечения безопасности потоков.

P.S.2 Экзотический случай, который может нарушить безопасность потоков неизменяемых коллекций, использует рефлексию для доступа к своим незавершенным полям.


Небольшое дополнение к другому экзотическому, но действительно ужасающему способу, как это было указано в комментариях к @Steve Waldman и @axel22 (автор). Если вы разделите неизменяемую коллекцию как часть некоторого объекта, совместно используемого между потоками && & если конструктор коллекции становится физически (по JIT) inlined (он по умолчанию не логически встроен) && & & если ваша JIT-реализация позволяет перестроить встроенный код с обычным - тогда вам нужно его синхронизировать (обычно этого достаточно, чтобы иметь @volatile). Тем не менее, IMHO, я не верю, что последнее условие - правильное поведение, но пока не может ни доказать, ни опровергнуть это.

Ответ 2

В вашем вопросе вы просите авторитетное выражение. Я нашел следующее в "Программирование в Scala" от Martin Odersky и др.: "В-третьих, нет возможности для двух потоков, одновременно обращающихся к неизменяемому, чтобы испортить его состояние, когда оно было правильно построено, потому что ни один поток не может изменить состояние неизменяемого"

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

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

Используемый здесь шаблон предназначен для создания и изменения объекта. Затем сделайте его видимым для других потоков, например, назначив этот экземпляр летучим статическим или статическим финалам. И после этого убедитесь, что он больше не изменен.

В качестве примера создаем два вектора:

  val vector = Vector(4,5,5)
  val vector2 =  vector.updated(1, 2);

Обновленный метод использует поле var, загрязненное внутри:

private[immutable] def updateAt[B >: A](index: Int, elem: B): Vector[B] = {
    val idx = checkRangeConvert(index)
    val s = new Vector[B](startIndex, endIndex, idx)
    s.initFrom(this)
    s.dirty = dirty
    s.gotoPosWritable(focus, idx, focus ^ idx)  // if dirty commit changes; go to new pos and prepare for writing
    s.display0(idx & 0x1f) = elem.asInstanceOf[AnyRef]
    s
  }

но так как после создания vector2 ему присваивается конечная переменная: Байт-код объявления переменной:

private final scala.collection.immutable.Vector vector2;

Байт-код конструктора:

61  invokevirtual scala.collection.immutable.Vector.updated(int, java.lang.Object, scala.collection.generic.CanBuildFrom) : java.lang.Object [52]
64  checkcast scala.collection.immutable.Vector [48]
67  putfield trace.agent.test.scala.TestVector$.vector2 : scala.collection.immutable.Vector [22]

Все - o.k.