簡體   English   中英

必須訪問scala.collection.immutable.List和Vector同步嗎?

[英]Must access to scala.collection.immutable.List and Vector be synchronized?

我將在Scala中學習並發編程 ,並遇到以下問題:

但是,在當前版本的Scala中,如果沒有同步,則無法共享某些被認為是不可變的集合,例如List和Vector。 雖然它們的外部API不允許您修改它們,但它們包含非最終字段。

提示:即使對象看起來是不可變的,也總是使用適當的同步來共享線程之間的任何對象。

來自Aleksandar Prokopec的Scala學習並發編程 ,第2章結尾(第58頁),Packt Publishing,2014年11月。

那可能是對的嗎?

我的工作假設一直是在描述為不可變的Scala庫數據結構中的任何內部可變性(實現懶惰,緩存等)都是冪等的,這樣在糟糕的競爭中可能發生的最壞情況是工作將不必要地重復。 這位作者似乎認為,通過並發訪問不可變結構可能會危害正確性。 真的嗎? 我們真的需要同步對列表的訪問嗎?

我轉向不可變重型風格的大部分原因都是為了避免同步以及它所帶來的潛在爭用開銷。 要了解Scala的核心“不可變”數據結構不能避免同步,這將是一件不愉快的大事。 這位作者是否過於保守?

Scala的集合文檔包括以下內容:

scala.collection.immutable包中的集合保證對每個人都是不可變的。 這樣的集合在創建后永遠不會改變。 因此,您可以依賴於在不同時間點重復訪問相同集合值將始終生成具有相同元素的集合的事實。

這並不能說它們對多個線程的並發訪問是安全的。 有沒有人知道他們是(或不是)的權威聲明?

這取決於你在哪里分享:

  • 在scala-library中共享它們是不安全的
  • 使用Java代碼,反射來共享它們是不安全的

簡單地說,這些集合的保護程度低於僅包含最終字段的對象。 無論它們在JVM級別上是否相同(沒有像ldc這樣的優化) - 兩者都可能是具有一些可變地址的字段,因此您可以使用putfield bytecode命令更改它們。 無論如何,與java的final ,scala的final valval相比, var仍然受編譯器的保護較少。

但是,在大多數情況下使用它們仍然很好,因為它們的行為在邏輯上是不可變的 - 所有可變操作都被封裝(對於Scala代碼)。 我們來看看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's filed 
      t = nx
      rest = rest.tail
    }
    h
}

所以這里t.tl = nx與線程安全意義上的t = nx差別不大。 它們都只是從單個堆棧( take堆棧)引用。 Althrought,如果我添加讓我們說someActor ! t someActor ! t (或任何其他異步操作), someField = tsomeFunctionWithExternalSideEffect(t)while循環內 - 我可以打破這個合約。


關於與JSR-133的關系,這里有一點補充:

1) new ::(head, Nil)在堆中創建新對象並將其地址(假設為0x100500)放入堆棧中( val h =

2)只要該地址在堆棧中,只有當前線程才知道

3)只有在將該地址放入某個字段后才能共享其他線程; 如果take它必須在調用areturnreturn h )之前刷新任何緩存(以恢復堆棧和寄存器),因此返回的對象將是一致的。

因此,只要0x100500只是堆棧的一部分(不是堆,而不是其他堆棧),0x100500對象上的所有操作都超出了JSR-133的范圍。 但是,0x100500對象的某些字段可能指向某些共享對象(可能在JSR-133范圍內),但這里不是這種情況(因為這些對象在外部是不可變的)。


我認為(希望)作者意味着圖書館開發人員的邏輯同步保證 - 如果你正在開發scala-library,你還需要小心這些東西,因為這些varprivate[scala]private[immutable]所以,它是可以編寫一些代碼來從不同的線程中改變它們。 從scala-library開發人員的角度來看,它通常意味着單個實例上的所有突變都應該應用於單個線程中,並且僅應用於對用戶不可見的集合(此時)。 或者,簡單地說 - 不要以任何方式為外部用戶打開可變字段。

PS Scala有幾個意外的同步問題,這導致庫的某些部分出乎意料地不是線程安全的,所以我不會懷疑是否有什么問題(這是一個錯誤),但讓我們說99% 99%方法的情況不可變集合是線程安全的。 在最壞的情況下,您可能會被某些破壞方法的使用推遲或只是(對於某些情況可能不僅僅是“只是”)需要克隆每個線程的集合。

無論如何,不​​變性仍然是線程安全的好方法。

PS2 Exotic案例可能會破壞不可變集合的線程安全性,它使用反射來訪問非最終字段。


正如@Steve Waldman和@ axel22(作者)的評論中指出的那樣,對於另一種異國情調但非常可怕的方式進行了一點補充。 如果你將不可變集合作為一些對象共享netween線程&&的成員共享,如果集合的構造函數在物理上(通過JIT)內聯(默認情況下它沒有邏輯內聯)&&如果你的JIT實現允許用正常的代碼重新排列內聯代碼 - 那么你有同步它(通常足以讓@volatile )。 但是,恕我直言,我不相信最后的條件是正確的行為 - 但是現在,既不能證明也不能反駁。

在你的問題中,你要求權威聲明。 我在Martin Odersky等人的“Scala編程”中找到了以下內容:“第三,一旦構造得當,兩個線程無法同時訪問一個不可變的狀態,因為沒有線程可以改變狀態一成不變”

如果您在實現中查看示例,您會看到在實現中遵循此操作,請參閱下文。

向量中有一些字段不是最終的,可能導致數據競爭。 但是因為它們只是在創建新實例的方法中更改,並且因為您需要同步操作來訪問不同線程中新創建的實例,無論如何都很好。

這里使用的模式是創建和修改對象。 比使其對其他線程可見,例如通過將此實例分配給易失性靜態或靜態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]

一切都好

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM