scala.collection.immutable.List と Vector へのアクセスは同期する必要がありますか? 質問する

scala.collection.immutable.List と Vector へのアクセスは同期する必要がありますか? 質問する

私は通過していますScala で並行プログラミングを学ぶ、次のような問題に遭遇しました。

ただし、現在のバージョンの Scala では、List や Vector など、不変と見なされる特定のコレクションは、同期なしでは共有できません。外部 API では変更できませんが、これらのコレクションには非 final フィールドが含まれています。

ヒント: オブジェクトが不変であるように見える場合でも、スレッド間でオブジェクトを共有するには、常に適切な同期を使用してください。

からScala で並行プログラミングを学ぶアレクサンダル・プロコペック著、第2章末尾(p.58)、Packt Publishing、2014年11月。

それは正しいでしょうか?

私の前提は、不変として記述された Scala ライブラリ データ構造内の内部可変性 (遅延、キャッシュなどを実装するため) は冪等であり、最悪の場合、競合が悪ければ作業が不必要に重複するだけだろうというものでした。この著者は、不変構造への同時アクセスによって正確性が損なわれる可能性があることを示唆しているようです。これは本当でしょうか。リストへのアクセスを同期させる必要があるのでしょうか。

私が不変重視のスタイルに移行した主な理由は、同期とそれに伴う潜在的な競合オーバーヘッドを避けたいという願望でした。Scala のコアである「不変」データ構造では同期を避けることができないと知ったら、それは残念なことです。この著者は単に保守的すぎるのでしょうか?

スカラのコレクションの文書化以下が含まれます:

scala.collection.immutable パッケージ内のコレクションは、誰に対しても不変であることが保証されています。このようなコレクションは、作成後に変更されることはありません。したがって、異なる時点で同じコレクション値に繰り返しアクセスすると、常に同じ要素を持つコレクションが生成されるという事実に頼ることができます。

これは、複数のスレッドによる同時アクセスが安全であるとは言い切れません。安全である (または安全でない) という権威ある声明を知っている人はいますか?

ベストアンサー1

によるどこ共有します:

  • scala-library内で共有するのは安全ではない
  • Javaコードやリフレクションと共有するのは安全ではない

簡単に言うと、これらのコレクションは、finalフィールドのみを持つオブジェクトよりも保護が弱いです。JVMレベルでは同じであるにもかかわらず(のような最適化なしldc)、どちらも変更可能なアドレスを持つフィールドである可能性があるため、バイトコードコマンドで変更できますputfield。とにかく、varはによって保護が弱いです。コンパイラfinal、JavaとScalaの比較final valそしてval

しかし、それらの動作は論理的に不変であるため、ほとんどの場合、それらを使用しても問題ありません。すべての可変操作はカプセル化されています(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保証の心配もありません。すべての変更可能な操作が実行されます。同じスレッドで( 、または を:+呼び出すスレッド)は、単なる初期化です。ここでの唯一の問題は、+:updatedprivate[somePackage] Javaコードから直接アクセス可能また、scala ライブラリ自体からも取得できるため、これを Java のメソッドに渡すと、メソッドが変更される可能性があります。

スレッドセーフ性について心配する必要はないと思います。短所演算子。また、変更可能なフィールドもあります。

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
}

しかし、それらは明示的な共有やスレッド作成なしでライブラリ メソッド内 (1 つのスレッド内) でのみ使用され、常に新しいコレクションを返します。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 = nxtakesomeActor ! tsomeField = 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 を開発している場合は、これらの点に注意する必要があります。これらvarは であるためprivate[scala]private[immutable]異なるスレッドから変更するコードを記述することが可能です。scala-library 開発者の観点からは、通常、単一インスタンスのすべての変更は単一スレッドで適用され、ユーザーには見えないコレクションに対してのみ適用されることを意味します (現時点では)。または、簡単に言うと、外部のユーザーに可変フィールドを一切公開しないでください。

PS Scalaは同期に関していくつかの予期せぬ問題を抱えており、図書館の一部驚いたことに、スレッドセーフではないので、何かが間違っているのではないかとは思いません (これはバグです)。しかし、99% のケースでは、99% のメソッドで不変コレクションはスレッドセーフです。最悪の場合、壊れたメソッドの使用から押し出されるか、または (場合によっては「単に」ではないかもしれませんが) スレッドごとにコレクションを複製する必要があります。

いずれにせよ、不変性は依然としてスレッドセーフのための良い方法です。

PS2 不変コレクションのスレッド セーフティを破る可能性がある特殊なケースとして、リフレクションを使用して非最終フィールドにアクセスすることが挙げられます。


@Steve Waldman と @axel22 (著者) のコメントで指摘されているように、別の風変わりだが本当に恐ろしい方法について少し追加します。不変コレクションをスレッド間で共有されるオブジェクトのメンバーとして共有する場合、コレクションのコンストラクターが物理的に (JIT によって) インライン化される場合 (デフォルトでは論理的にインライン化されません)、JIT 実装によってインライン化されたコードを通常のコードと再配置できる場合は、同期する必要があります (通常は で十分です@volatile)。ただし、私見では、最後の条件が正しい動作であるとは思いませんが、今のところ、それを証明することも反証することもできません。

おすすめ記事