Scala:对象初始化程序中的并行收集导致程序挂起

时间:2013-03-02 15:36:09

标签: scala scala-collections

我刚注意到一种令人不安的行为。 让我们说我有一个独立的程序,包含一个唯一的对象:

object ParCollectionInInitializerTest {
  def doSomething { println("Doing something") }

  for (i <- (1 to 2).par) {
    println("Inside loop: " + i)
    doSomething
  }

  def main(args: Array[String]) {
  }
}

该程序是完全无辜的,当for循环中使用的范围并行时,正确执行,输出如下:

  

内循环:1
  做某事
  内循环:2
  做点什么

不幸的是,在使用并行集合时,程序只是挂起而没有调用doSomething方法,所以输出如下:

  

内循环:2
  内循环:1

然后程序挂起。
这只是一个讨厌的bug吗?我使用的是scala-2.10。

1 个答案:

答案 0 :(得分:25)

这是Scala在构造完成之前释放对单例对象的引用时可能发生的固有问题。它发生的原因是不同的线程在完全构造之前尝试访问对象ParCollectionInInitializerTest。 它与main方法无关,而是与初始化包含main方法的对象有关 - 尝试在REPL中运行它,输入表达式{{1}你会得到相同的结果。它也与fork-join工作线程是守护程序线程没有任何关系。

单例对象被懒惰地初始化。每个单例对象只能初始化一次。这意味着访问对象的第一个线程(在您的情况下,主线程)必须获取对象的锁,然后初始化它。随后出现的每个其他线程都必须等待主线程初始化对象并最终释放锁。这是在Scala中实现单例对象的方式。

在您的情况下,并行收集工作线程尝试访问单个对象以调用ParCollectionInInitializerTest,但在主线程完成初始化对象之前不能这样做 - 所以它等待。另一方面,主线程在构造函数中等待,直到并行操作完成,这是以所有工作线程完成为条件的 - 主线程始终保持单例的初始化锁定。因此,发生了死锁。

您可以使用2.10的期货或单纯线程导致此行为,如下所示:

doSomething

将其粘贴到REPL中,然后写:

def execute(body: =>Unit) {
  val t = new Thread() {
    override def run() {
      body
    }
  }

  t.start()
  t.join()
}

object ParCollection {

  def doSomething() { println("Doing something") }

  execute {
    doSomething()
  }

}

并且REPL挂起。