18

我刚刚注意到一个令人不安的行为。假设我有一个由一个对象组成的独立程序:

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

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

4

1 回答 1

28

这是在 Scala 中在构造完成之前释放对单例对象的引用时可能发生的固有问题。ParCollectionInInitializerTest发生这种情况是因为在对象完全构造之前,另一个线程试图访问该对象。它与方法无关main,而是与初始化包含该main方法的对象有关——尝试在 REPL 中运行它,输入表达式ParCollectionInInitializerTest,您将得到相同的结果。它也与作为守护线程的 fork-join 工作线程无关。

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

在您的情况下,并行收集工作线程尝试访问要调用的单例对象doSomething,但在主线程完成初始化对象之前不能这样做 - 所以它会等待。另一方面,主线程在构造函数中等待,直到并行操作完成,这取决于所有工作线程完成——主线程一直持有单例的初始化锁。因此,出现死锁。

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

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 中,然后编写:

scala> ParCollection

并且 REPL 挂起。

于 2013-03-02T15:59:57.503 回答