3

下面的(人为的)代码尝试在未来打印一个按名称的字符串参数,并在打印完成时返回。

import scala.concurrent._
import concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._

class PrintValueAndWait {
  def printIt(param: => String): Unit = {
    val printingComplete = future { 
      println(param);  // why does this hang?
    }
    Await.result(printingComplete, Duration.Inf)
  }
}

object Go {
  val str = "Rabbits"

  new PrintValueAndWait().printIt(str)
}

object RunMe extends App {
  Go
}

但是,在运行时RunMe,它只是在尝试评估时挂起param。更改printIt为按值接收其参数会使应用程序按预期返回。或者,更改printIt为简单地打印值并同步返回(在同一个线程中)似乎也可以正常工作。

这里到底发生了什么?这是否与尚未完全构建的 Go 对象有关,因此该str字段对于尝试打印它的线程尚不可见?在这里挂起预期的行为吗?

我已经在 J​​ava 1.7 上的 Mac OS Mavericks 和 Windows 7 上使用 Scala 2.10.3 进行了测试。

4

1 回答 1

10

您的代码在Go对象初始化时陷入僵局。这是一个已知问题,参见例如SI-7646和这个SO question

scala 中的对象被延迟初始化,并在此期间锁定以防止两个线程竞相初始化对象。但是,如果两个线程同时尝试初始化一个对象,一个依赖另一个完成,就会出现循环依赖和死锁。

在这种特殊情况下,Go对象的初始化只能new PrintValueAndWait().printIt(str)在完成后完成。但是,whenparam是一个按名称参数,本质上是一个代码块被传入,在使用它时会对其进行评估。在这种情况下,str参数 innew PrintValueAndWait().printIt(str)是 的简写Go.str,因此当未来运行的线程尝试评估param它时,它实际上是在调用Go.str. 但是由于Go还没有完成初始化,它也会尝试初始化Go对象。另一个线程初始化Go对其初始化有一个锁定,因此未来的线程会阻塞。所以第一个线程在完成初始化之前等待未来完成,而未来线程正在等待第一个线程完成初始化:死锁。

在按值的情况下,str直接传入的是字符串值,所以未来线程不会尝试初始化Go,不会出现死锁。

同样,如果您param按名称离开,但更改Go如下:

object Go {
  val str = "Rabbits"

  {
    val s = str
    new PrintValueAndWait().printIt(s)
  }
}

它不会死锁,因为s传入了已经评估的本地字符串值,而不是Go.str,所以未来的线程不会尝试初始化Go

于 2013-11-23T04:51:21.503 回答