为什么scala会挂起评估Future中的by-name参数?

时间:2013-11-23 02:58:27

标签: multithreading scala object future pass-by-name

下面(设计的)代码尝试在将来打印一个名字的String参数,并在打印完成时返回。

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更改为接受其参数by-value会使应用程序按预期返回。或者,将printIt更改为简单地打印值并同步返回(在同一个线程中)似乎也可以正常工作。

这里到底发生了什么?这是否与Go对象有关尚未完全构造,因此str字段尚未显示给试图打印它的线程?挂在这里的预期行为?

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

1 个答案:

答案 0 :(得分:10)

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

scala中的对象被懒惰地初始化,并且在此期间进行锁定以防止两个线程竞争以初始化对象。但是,如果两个线程同时尝试初始化一个对象而另一个线程依赖另一个完成,则会出现循环依赖和死锁。

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

在by值的情况下,str的字符串值直接传入,因此未来的线程不会尝试初始化Go并且没有死锁。

同样,如果您按名称保留param,但按以下方式更改Go

object Go {
  val str = "Rabbits"

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

它不会死锁,因为传入已经计算的本地字符串值s而不是Go.str,因此将来的线程不会尝试初始化Go。< / p>