【问题标题】:Scala Stream tail laziness and synchronizationScala Stream尾延迟和同步
【发布时间】:2018-01-05 10:09:46
【问题描述】:

在他的一个视频中(关于 Scala 的惰性求值,即 lazy 关键字),Martin Odersky 展示了用于构造 Streamcons 操作的以下实现:

def cons[T](hd: T, tl: => Stream[T]) = new Stream[T] {
  def head = hd
  lazy val tail = tl
  ...
}

所以tail 操作是使用语言的惰性求值功能简洁地编写的。

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

@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
}

双重检查锁定和两个 v​​olatile 字段:这大致就是您在 Java 中实现线程安全惰性计算的方式。

所以问题是

  1. Scala 的 lazy 关键字在多线程情况下不提供任何“评估最大值一次”保证吗?
  2. 在真正的tail 实现中使用的模式是在 Scala 中进行线程安全惰性求值的惯用方式吗?

【问题讨论】:

  • 正在开发的新系列中的 FWIW the implementation 要简单得多。
  • @Jasper-M 这个新实现是否在多线程情况下提供了“评估最大值一次”的保证?如果是,它是如何实现的?对于愚蠢的问题,我很抱歉,但我是 Scala 的新手,我看不出该代码与 Martin 最初在幻灯片上展示的代码有任何主要区别。
  • 我不知道。您必须询问开发人员。
  • 我也不理解这个新实现,因为据我所知,每次访问方法 tail 时都会对其进行评估。
  • @L.Lampart 如果我没记错的话,唯一需要重新评估的是空值检查(因为该字段是易变的),这非常快。

标签: multithreading scala lazy-evaluation


【解决方案1】:

Scala 的惰性关键字不提供任何“评估最大值一次” 在多线程情况下保证?

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

在真正的尾部实现中使用的模式是一种惯用的方式吗 Scala 中的线程安全惰性求值?

编辑:

我想我有实际的答案,为什么不lazy valStream 具有面向公众的 API 方法,例如继承自 TraversableOncehasDefinitionSize。为了知道Stream 的大小是否有限,我们需要一种方法来检查而不实现底层Stream 尾部。由于lazy val 实际上并没有暴露底层位,所以我们不能这样做。

这是由SI-1220支持的

为了加强这一点,@Jasper-M 指出稻草人中的新 LazyList api(Scala 2.13 集合改造)不再有这个问题,因为整个集合层次结构已经过重新设计,不再存在这样的问题.


性能相关问题

我会说“这取决于”您从哪个角度看待这个问题。从 LOB 的角度来看,为了实现的简洁和清晰,我肯定会选择 lazy val。但是,如果您从 Scala 集合库作者的角度来看,事情就会开始有所不同。这样想,您正在创建一个可能被许多人使用并在世界各地的许多机器上运行的库。这意味着您应该考虑每个结构的内存开销,尤其是在您自己创建这样一个重要的数据结构时。

我这样说是因为当您使用lazy val 时,根据设计,您会生成一个额外的Boolean 字段,该字段会标记该值是否已初始化,并且我假设这是库作者旨在避免的。 JVM 上Boolean 的大小当然取决于 VM,甚至一个字节也需要考虑,尤其是当人们生成大量 Streams 数据时。同样,这绝对不是我通常会考虑的,而且绝对是对内存使用的微优化。

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

实现的区别在于tail的定义是否被初始化是一个检查生成器的方法实现:

def tailDefined: Boolean = tlGen eq null

而不是类上的字段。

【讨论】:

    【解决方案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>();
          ()
        }
      }
    }
    

    您应该能够看到...惰性求值是线程安全的。您还会看到与 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 翻译更有效的解决方案。

    【讨论】:

      【解决方案3】:
      1. 你是对的,lazy vals 在两个线程同时访问时精确地使用锁定来防止双重评估。此外,未来的发展将在不锁定的情况下提供相同的保证。
      2. 在我看来,什么是惯用语是一个备受争议的话题,因为它通过设计允许采用各种不同的惯用语。然而,总的来说,当更多地进入纯函数式编程的方向时,应用程序代码往往被认为是惯用的,因为它在易于测试和推理方面提供了一系列有趣的优势,只有在以下情况下才有意义放弃严重关切。这个问题可能是性能问题之一,这就是为什么当前的 Scala Collection API 实现,虽然在大多数情况下暴露了一个功能接口,但大量使用(在内部和受限范围内)vars、while 循环和命令式编程的既定模式(正如您在问题中强调的那样)。

      【讨论】:

        猜你喜欢
        • 2012-08-10
        • 1970-01-01
        • 2021-04-16
        • 1970-01-01
        • 1970-01-01
        • 2011-10-18
        • 2019-06-01
        • 2014-01-15
        • 1970-01-01
        相关资源
        最近更新 更多