必须访问scala.collection.immutable.List和Vector同步吗?

时间:2015-04-18 06:14:54

标签: multithreading scala concurrency

我正在浏览Learning Concurrent Programming in Scala,并遇到以下情况:

  

然而,在当前版本的Scala中,某些集合是   如果没有,则无法共享被视为不可变的内容,例如List和Vector   同步。虽然他们的外部API不允许你这样做   修改它们,它们包含非最终字段。

     

提示:即使是一个对象   似乎是不可变的,总是使用适当的同步来共享任何对象   线程之间。

来自Aleksandar Prokopec的学习Scala中的并发编程,第2章结尾(第58页),Packt Publishing,2014年11月。

那可能是对的吗?

我的工作假设一直是在Scala库数据结构中描述为不可变的任何内部可变性(实现懒惰,缓存等等)都是幂等的,这样在糟糕的竞争中可能发生的最糟糕的工作就是不必要的复制。这位作者似乎认为,通过并发访问不可变结构可能会危害正确性。真的吗?我们真的需要同步对列表的访问吗?

我转向不可变重型风格的大部分原因都是为了避免同步以及它带来的潜在争用开销。要知道Scala的核心"不可变的"并不能避免同步,这将是一件不愉快的大事。数据结构。这位作者是否过于保守?

Scala的documentation of collections包括以下内容:

  

scala.collection.immutable包中的集合保证对每个人都是不可变的。这样的集合在创建后永远不会改变。因此,您可以依赖这样的事实:在不同的时间点重复访问相同的集合值将始终产生具有相同元素的集合。

这并不能说它们对多个线程的并发访问是安全的。有没有人知道他们是(或不是)的权威声明?

2 个答案:

答案 0 :(得分:6)

这取决于您分享的地方

  • 在scala-library中分享它们是不安全的
  • 与Java代码共享它们是不安全的,反思

简单地说,这些集合的保护程度低于只有最终字段的对象。无论它们在JVM级别上是否相同(没有ldc之类的优化) - 两者都可能是具有一些可变地址的字段,因此您可以使用putfield字节码命令更改它们。无论如何,{em>编译器对var的保护程度仍然较低,与java final,scala' final val和{{1}相比较}}

但是,在大多数情况下使用它们仍然很好,因为它们的行为在逻辑上是不可变的 - 所有可变操作都被封装(对于Scala代码)。让我们看一下Vector。它需要可变字段来实现附加算法:

val

实现如下:

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 is accessible directly from Java code和scala-library本身,所以如果你把它传递给某些Java方法,它可以修改它们。

我不认为你应该担心让cons运营商说出线程安全问题。它也有可变领域:

private[somePackage]

但他们只使用内部库方法(在一个线程内)没有任何显式共享或线程创建,并且他们总是返回一个新集合,让我们以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's filed t = nx rest = rest.tail } h } 与线程安全意义上的t.tl = nx差别不大。它们都仅来自单个堆栈(t = nx的堆栈)。如果我在take循环中添加让我们说someActor ! t(或任何其他异步操作),someField = tsomeFunctionWithExternalSideEffect(t) - 我可以打破此合同


这里有一点关于与JSR-133的关系:

1)while在堆中创建新对象并将其地址(假设为0x100500)放入堆栈(new ::(head, Nil)

2)只要该地址在堆栈中,它只对当前线程知道

3)只有在将此地址放入某个字段后才能涉及其他线程;在val h =的情况下,它必须在调用takeareturn)之前刷新任何缓存(以恢复堆栈和寄存器),因此返回的对象将是一致的。

因此,0x100500对象的所有操作都超出了JSR-133的范围,只要0x100500只是堆栈的一部分(不是堆,而不是其他堆栈)。但是,0x100500对象的某些字段可能指向某些共享对象(可能在JSR-133范围内),但这里不是这种情况(因为这些对象在外部是不可变的)。


我认为(希望)作者意味着图书馆开发人员的逻辑同步保证 - 如果你正在开发scala-library,你仍然需要小心这些事情,因为这些return h s是varprivate[scala]因此,可以编写一些代码来从不同的线程中改变它们。从scala-library开发人员的角度来看,它通常意味着单个实例上的所有突变都应该应用于单个线程中,并且仅应用于对用户不可见的集合(此时)。或者,简单地说 - 不要以任何方式为外部用户打开可变字段。

P.S。 Scala有几个意外的同步问题,这导致一些parts of the library出乎意料地不是线程安全的,所以我不会想知道是否有什么问题(这是一个错误),但是在#&#中39; s表示99%的99%方法的不可变集合是线程安全的。在最坏的情况下,您可能会被某些破坏方法的使用推迟或只是(可能不仅仅是#34;只是&#34;对于某些情况)需要克隆每个线程的集合。

无论如何,不​​变性仍然是线程安全的好方法。

P.S.2可能破坏不可变集合的外来案例&#39; thread-safety使用反射来访问非最终字段。


在@Steve Waldman和@ axel22(作者)的评论中指出了另一种异国情调但非常可怕的方式。如果您将不可变集合作为某些对象共享的netween线程&amp;&amp;的成员共享如果集合的构造函数在物理上(通过JIT)内联(默认情况下它没有逻辑内联)&amp;&amp;如果您的JIT实现允许使用正常代码重新排列内联代码 - 那么您必须同步它(通常足以拥有private[immutable])。但是,恕我直言,我不相信最后的条件是正确的行为 - 但是现在,既不能证明也不反驳。

答案 1 :(得分:2)

在你的问题中,你要求一份权威声明。我在&#34; Scala编程&#34;中找到了以下内容:来自Martin Odersky等人: &#34;第三,一旦两个线程被合理构造,就没有办法同时访问一个不可变的来破坏它的状态,因为没有线程可以改变一个不可变的状态&#34;

如果您在实现中查看示例,您会看到在实现中遵循此操作,请参阅下文。

向量中有一些字段不是最终的,可能导致数据竞争。但是因为它们只是在创建新实例的方法中更改,并且因为您需要同步操作来访问不同线程中新创建的实例,无论如何都很好。

此处使用的模式是创建和修改对象。比使其对其他线程可见,例如通过将此实例分配给易失性静态或静态final。然后确保它不再改变。

作为示例创建两个向量:

  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.