Scala中的多线程 - 仅处理不变性

时间:2013-06-15 12:35:27

标签: java multithreading scala

我有Scala的代码

class MyClass {
  private val myData: Map[String, MyClass2] = new HashMap[String, MyClass2]()

  def someMethod = {
      synchronized(myData) {
        val id = getSomeId
        if (myData.containsKey(id)) myData = myData.remove(id)
        else Log.d("123", "Not found!")
      }
  }

  def getSomeId = //....
}

我想知道,是否可以在不使用synchronized的情况下保持此代码的线程安全,并且不涉及其他一些库,例如Akka或任何其他(类)甚至内置Java或Scala?

理想情况下,我只想通过使用不可变性的概念(如果你愿意在Java中使用final)来创建线程安全。

更新

class MyClass(myData: Map[String, MyClass2] = new HashMap[String, MyClass2]()) {

  def someMethod = {
      synchronized(myData) {
        val id = getSomeId
        if (myData.containsKey(id)) new MyClass(myData.remove(id))
        else {
           Log.d("123", "Not found!")
           this
         }
      }
  }

  def getSomeId = //....
}

4 个答案:

答案 0 :(得分:4)

只有在MyClass不可变的情况下(并且只允许它使用不可变数据结构),你才能用不变性解决问题。原因很简单:如果MyClass是可变的,那么您必须通过并发线程同步修改。

这需要不同的设计 - 导致MyClass实例“更改”的每个操作都将返回一个(可能)修改过的实例。

import collection.immutable._

class MyClass2 {
  // ...
}

// We can make the default constructor private, if we want to manage the
// map ourselves instead of allowing users to pass arbitrary maps
// (depends on your use case):
class MyClass private (val myData: Map[String,MyClass2]) {
  // A public constructor:
  def this() = this(new HashMap[String,MyClass2]())

  def someMethod(id: String): MyClass = {
    if (myData.contains(id))
      new MyClass(myData - id) // create a new, updated instance
    else {
      println("Not found: " + id)
      this // no modification, we can return the current
            // unmodified instance
    }
  }

  // other methods for adding something to the map
  // ...
}

答案 1 :(得分:2)

如果你使用来自scala 2.10的TrieMap,这是一个无锁且并发的地图实现,你可以避免同步:

import scala.collection.concurrent.TrieMap

class MyClass2

class MyClass {
    private val myData = TrieMap[String, MyClass2]()
    def someMethod = myData -= getSomeId
    def getSomeId = "id"
}

答案 2 :(得分:1)

我建议使用一个库,因为你自己获得并发是很难。例如,您可以使用TrieMap之类的并发映射。见上面的答案。

但我们假设你想手动为教育目的这样做。使上述线程安全的第一步是使用不可变集合。而不是

private val myData: Map[String, MyClass2] = new HashMap[String, MyClass2]()

你会用

private var myData = Map.empty[String, MyClass2] 

(即使这里有一个var,它的可变状态也比上面的版本少。在这种情况下,唯一可变的东西是单个引用,而在上面的例子中整个集合是可变的)

现在你必须处理var。您必须确保在所有其他线程上“看到”对一个线程上的var的更新。所以你必须将该字段标记为@volatile。如果您有一个发布/订阅场景,只能从一个线程完成写入,那就足够了。但假设您希望从不同的线程读取和写入,则需要对所有 write 访问使用synchronized。

显然,这足够复杂,需要引入一个小帮助类:

final class SyncronizedRef[T](initial:T) {
  @volatile private var current = initial

  def get:T = current

  def update(f:T=>T) {
    this synchronized {
      current = f(current)
    }
  }
}

有了这个小助手,上面的代码可以这样实现:

class MyClass {
  val state = new SyncronizedRef(Map.empty[String, MyClass2])

  def someMethod = {
    state.update(myData =>
      val id = getSomeId
      if (myData.containsKey(id)) 
        myData - id
      else { 
        Log.d("123", "Not found!")
        myData
      }
  }

  def getSomeId = //....
}

就地图而言,这将是线程安全的。但是,整个事物是否是线程安全取决于getSomeID中发生的任何事情。

一般来说,这种处理并发的方法只要传递给update的东西是一个 pure 函数,只是转换数据而没有任何副作用。如果您的状态比单个地图更复杂,那么以纯粹的功能样式编写更新可能非常具有挑战性。

SynchronizedRef中仍然存在低级多线程原语,但程序的逻辑完全没有它们。您只需通过编写纯函数来描述程序状态如何响应外部输入而发生变化。

在任何情况下,此特定示例的最佳解决方案就是使用现有的并发映射实现。

答案 3 :(得分:1)

每当你共享可变状态时,你需要一个并发机制。正如Rüdiger所指出的,最好的方法是确定您拥有哪种并发方案,然后使用最适合该方案的现有工具:

  • 普通的旧Java同步锁
  • 原子比较和交换
  • 软件事务存储器(例如Scala-STM)
  • 消息传递/演员

当然,如果您不需要尽可能高的性能,您可以使用协作式多任务处理(在单个共享线程上运行并发进程)。

如果您的班级的状态是可监督的,您可以使该班级完全不可变,例如:

case class MyClass2()

case class MyClass(myData: Map[String, MyClass2] = Map.empty) {
  def someMethod = {
    val id = getSomeId
    if (myData.contains(id)) copy(myData = myData - id)
    else throw new IllegalArgumentException(s"Key $id not found")
  }

  def getSomeId = "foo" // ...
}

只有通过将数据复制到新实例来突变MyClass的实例,因此多个线程可以安全地引用同一个实例。但另一方面,如果两个线程A和B以相同的实例foo1开始,并且它们中的任何一个突变它并且希望该突变被另一个线程看到,那么您需要以某种形式再次共享该变异状态(使用STM ref单元格,通过actor发送消息,将其存储在同步变量等中。)

val foo1 = MyClass(Map("foo" -> MyClass2()))
val foo2 = foo1.someMethod  // foo1 is untouched