Scala Stream尾部懒惰和同步

时间:2018-01-05 10:09:47

标签: multithreading scala lazy-evaluation

在他的一个视频中(关于Scala的懒惰评估,即lazy关键字),Martin Odersky展示了用于构建cons的{​​{1}}操作的以下实现:

Stream

因此使用该语言的惰性评估功能简明扼要地编写def cons[T](hd: T, tl: => Stream[T]) = new Stream[T] { def head = hd lazy val tail = tl ... } 操作。

但实际上(在Scala 2.11.7中),tail的实现有点不那么优雅了:

tail

双重检查锁定和两个易失性字段:这大致是如何在Java中实现线程安全的惰性计算。

所以问题是

  1. Scala的@volatile private[this] var tlVal: Stream[A] = _ @volatile private[this] var tlGen = tl _ def tailDefined: Boolean = tlGen eq null override def tail: Stream[A] = { if (!tailDefined) synchronized { if (!tailDefined) { tlVal = tlGen() tlGen = null } } tlVal } 关键字在多线程案例中是否提供任何“评估的最大一次”保证?
  2. 真实lazy实现中使用的模式是否是在Scala中进行线程安全延迟评估的惯用方法?

3 个答案:

答案 0 :(得分:4)

  

Scala的懒惰关键字不提供任何“已评估的最大值”   在多线程案例中保证?

是的,正如其他人所说的那样。

  

真正的尾部实现中使用的模式是否是惯用的方法   Scala中一个线程安全的懒惰评估?

编辑:

我认为我有实际的回答为什么不是lazy valStream具有公开的API方法,例如hasDefinitionSize继承自TraversableOnce。为了了解Stream是否具有有限大小,我们需要一种检查的方法,而不会实现基础 Stream尾部。由于lazy val实际上没有公开底层位,我们不能这样做。

这由SI-1220

支持

为了加强这一点,@ Jasper-M指出草丛中的新LazyList api(Scala 2.13集合改造版)不再存在这个问题,因为整个集合层次结构已经重新设计并且不再存在这样的担忧。

与绩效相关的问题

我会说“这取决于”你在看这个问题的角度。从LOB的角度来看,我肯定会说明lazy val的简洁性和清晰度。但是,如果从Scala集合库作者的角度来看它,事情开始变得不同。想象一下,你正在创建一个可能被许多人使用并在世界各地的许多机器上运行的库。这意味着你应该考虑每个结构的内存开销,特别是如果你自己创建这样一个基本的数据结构。

我说这是因为当你使用lazy val时,你会设计一个额外的Boolean字段来标记该值是否已经初始化,我假设这是图书馆作者的目标。避免。 JVM上Boolean的大小当然取决于VM,甚至需要考虑一个字节,特别是当人们生成大Stream个数据时。同样,这绝对是我通常会考虑的东西,绝对是对内存使用的微优化。

我认为性能是关键点之一的原因是SI-7266修复了Stream中的内存泄漏。请注意跟踪字节代码以确保在生成的类中没有保留额外值的重要性。

实现的不同之处在于正在初始化的tail的定义是检查生成器的方法实现:

def tailDefined: Boolean = tlGen eq null

而不是班上的一个字段。

答案 1 :(得分:2)

Scala lazy值仅在多线程情况下评估一次。这是因为lazy成员的评估实际上包含在生成的代码中的同步块中。

让我们来看看简单的claas,

class LazyTest {

  lazy val x = 5

}

现在,让我们用scalac编译它,

scalac -Xprint:all LazyTest.scala

这将导致

package <empty> {
  class LazyTest extends Object {
    final <synthetic> lazy private[this] var x: Int = _;
    @volatile private[this] var bitmap$0: Boolean = _;
    private def x$lzycompute(): Int = {
      LazyTest.this.synchronized(if (LazyTest.this.bitmap$0.unary_!())
        {
          LazyTest.this.x = (5: Int);
          LazyTest.this.bitmap$0 = true
        });
      LazyTest.this.x
    };
    <stable> <accessor> lazy def x(): Int = if (LazyTest.this.bitmap$0.unary_!())
      LazyTest.this.x$lzycompute()
    else
      LazyTest.this.x;
    def <init>(): LazyTest = {
      LazyTest.super.<init>();
      ()
    }
  }
}

你应该能够看到......懒惰的评估是线程安全的。而且你也会看到一些相似之处&#34;不太优雅&#34;在Scala 2.11.7中实现

您还可以尝试类似以下的测试,

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

case class A(i: Int) {

  lazy val j = {
    println("calculating j")
    i + 1
  }

}

def checkLazyInMultiThread(): Unit = {

  val a = A(6)

  val futuresList = Range(1, 20).toList.map(i => Future{
    println(s"Future $i :: ${a.j}")
  })

  Future.sequence(futuresList).onComplete(_ => println("completed"))

}

checkLazyInMultiThread()

现在,标准库中的实现避免使用lazy,因为它们能够提供比此通用lazy转换更有效的解决方案。

答案 2 :(得分:1)

  1. 你是对的,lazy val使用锁定来防止双重评估,当两个线程同时访问时。此外,未来的发展将在没有锁定的情况下提供相同的保证。
  2. 在我的拙见中,什么是惯用的,当谈到一种语言,通过设计,允许采用各种不同的习语时,这是一个备受争议的主题。然而,一般而言,当更多地进入纯函数式编程的方向时,应用程序代码往往被认为是惯用的,因为它在易于测试和推理方面提供了一系列有趣的优点,只有在以下情况下放弃才有意义。严重关切。这种关注可能是性能问题之一,这就是为什么Scala Collection API的当前实现虽然在大多数情况下暴露了功能接口,却大量使用var s while {{1}来自命令式编程的循环和已建立的模式(正如您在问题中强调的那样)。