Scala Stream 尾延迟和同步
Scala Stream tail laziness and synchronization
在他的一个视频中(关于 Scala 的惰性求值,即 lazy
关键字),Martin Odersky 展示了用于构造 Stream
的 cons
操作的以下实现:
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
}
双重检查锁定和两个可变字段:这就是您在 Java.
中实现线程安全惰性计算的大致方式
所以问题是:
- Scala 的
lazy
关键字在多线程情况下不提供任何 'evaluated maximum once' 保证吗?
- 实际
tail
实现中使用的模式是在 Scala 中执行线程安全惰性求值的惯用方式吗?
- 你是对的,
lazy val
s 使用锁定精确地防止两个线程同时访问时的双重计算。此外,未来的发展将在没有锁定的情况下提供相同的保证。
- 在我看来,当涉及到一种语言时,什么是惯用语是一个非常有争议的话题,这种语言在设计上允许采用各种不同的惯用语。然而,一般来说,当更多地转向纯函数式编程的方向时,应用程序代码往往被认为是惯用的,因为它在易于测试和推理方面提供了一系列有趣的优势,只有在以下情况下才有意义放弃严重的担忧。这种担忧可能是一种性能问题,这就是为什么 Scala Collection API 的当前实现在大多数情况下公开功能接口时大量使用(内部和受限范围内)
var
s ,while
循环并从命令式编程中建立模式(如您在问题中突出显示的模式)。
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[=12=]: Boolean = _;
private def x$lzycompute(): Int = {
LazyTest.this.synchronized(if (LazyTest.this.bitmap[=12=].unary_!())
{
LazyTest.this.x = (5: Int);
LazyTest.this.bitmap[=12=] = true
});
LazyTest.this.x
};
<stable> <accessor> lazy def x(): Int = if (LazyTest.this.bitmap[=12=].unary_!())
LazyTest.this.x$lzycompute()
else
LazyTest.this.x;
def <init>(): LazyTest = {
LazyTest.super.<init>();
()
}
}
}
您应该能够看到...惰性求值是线程安全的。您还会看到与 Scala 2.11.7
中 "less elegant" 实现的一些相似之处
您也可以尝试类似以下的测试,
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
翻译更有效的解决方案。
Doesn't lazy keyword of Scala provide any 'evaluated maximum once'
guarantee in a multi-threaded case?
是的,就像其他人所说的那样。
Is the pattern used in real tail implementation an idiomatic way to do
a thread-safe lazy evaluation in Scala?
编辑:
我想我有 实际的 答案,为什么不 lazy val
。 Stream
有 public 面向 API 方法,例如 hasDefinitionSize
继承自 TraversableOnce
。为了知道 Stream
是否具有有限大小,我们需要一种方法来检查 而无需具体化底层 Stream
尾部。由于 lazy val
实际上并未公开底层位,因此我们不能这样做。
这得到了 SI-1220
的支持
为了加强这一点,@Jasper-M 指出新的 LazyList
api in strawman (Scala 2.13 collection makeover) 不再有这个问题,因为整个集合层次结构已经重做,不再有这样的顾虑。
与性能相关的问题
我会说 "it depends" 你是从哪个角度看这个问题的。从 LOB 的角度来看,为了实现的简洁明了,我肯定会选择 lazy val
。但是,如果您从 Scala 集合库作者的角度来看它,事情就会开始有所不同。以这种方式思考,您正在创建一个库,它可能会被许多人使用,并且 运行 在世界各地的许多机器上使用。这意味着您应该考虑每个结构的内存开销,尤其是当您自己创建这样一个基本数据结构时。
我这样说是因为当你使用 lazy val
时,你会按设计生成一个额外的 Boolean
字段,该字段会标记该值是否已初始化,我假设这就是库作者的意思旨在避免。 JVM 上 Boolean
的大小当然取决于 VM,即使是一个字节也是需要考虑的事情,尤其是当人们生成大量 Stream
数据时。同样,这绝对 不是 我通常会考虑的事情,并且绝对是对内存使用的微优化。
我认为性能是这里的关键点之一的原因是 SI-7266 它修复了 Stream 中的内存泄漏。请注意跟踪字节码以确保在生成的 class.
中没有保留额外值的重要性。
实现上的区别在于tail
初始化与否的定义是检查生成器的方法实现:
def tailDefined: Boolean = tlGen eq null
而不是 class 上的字段。
在他的一个视频中(关于 Scala 的惰性求值,即 lazy
关键字),Martin Odersky 展示了用于构造 Stream
的 cons
操作的以下实现:
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
}
双重检查锁定和两个可变字段:这就是您在 Java.
中实现线程安全惰性计算的大致方式所以问题是:
- Scala 的
lazy
关键字在多线程情况下不提供任何 'evaluated maximum once' 保证吗? - 实际
tail
实现中使用的模式是在 Scala 中执行线程安全惰性求值的惯用方式吗?
- 你是对的,
lazy val
s 使用锁定精确地防止两个线程同时访问时的双重计算。此外,未来的发展将在没有锁定的情况下提供相同的保证。 - 在我看来,当涉及到一种语言时,什么是惯用语是一个非常有争议的话题,这种语言在设计上允许采用各种不同的惯用语。然而,一般来说,当更多地转向纯函数式编程的方向时,应用程序代码往往被认为是惯用的,因为它在易于测试和推理方面提供了一系列有趣的优势,只有在以下情况下才有意义放弃严重的担忧。这种担忧可能是一种性能问题,这就是为什么 Scala Collection API 的当前实现在大多数情况下公开功能接口时大量使用(内部和受限范围内)
var
s ,while
循环并从命令式编程中建立模式(如您在问题中突出显示的模式)。
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[=12=]: Boolean = _;
private def x$lzycompute(): Int = {
LazyTest.this.synchronized(if (LazyTest.this.bitmap[=12=].unary_!())
{
LazyTest.this.x = (5: Int);
LazyTest.this.bitmap[=12=] = true
});
LazyTest.this.x
};
<stable> <accessor> lazy def x(): Int = if (LazyTest.this.bitmap[=12=].unary_!())
LazyTest.this.x$lzycompute()
else
LazyTest.this.x;
def <init>(): LazyTest = {
LazyTest.super.<init>();
()
}
}
}
您应该能够看到...惰性求值是线程安全的。您还会看到与 Scala 2.11.7
中 "less elegant" 实现的一些相似之处您也可以尝试类似以下的测试,
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
翻译更有效的解决方案。
Doesn't lazy keyword of Scala provide any 'evaluated maximum once' guarantee in a multi-threaded case?
是的,就像其他人所说的那样。
Is the pattern used in real tail implementation an idiomatic way to do a thread-safe lazy evaluation in Scala?
编辑:
我想我有 实际的 答案,为什么不 lazy val
。 Stream
有 public 面向 API 方法,例如 hasDefinitionSize
继承自 TraversableOnce
。为了知道 Stream
是否具有有限大小,我们需要一种方法来检查 而无需具体化底层 Stream
尾部。由于 lazy val
实际上并未公开底层位,因此我们不能这样做。
这得到了 SI-1220
的支持为了加强这一点,@Jasper-M 指出新的 LazyList
api in strawman (Scala 2.13 collection makeover) 不再有这个问题,因为整个集合层次结构已经重做,不再有这样的顾虑。
与性能相关的问题
我会说 "it depends" 你是从哪个角度看这个问题的。从 LOB 的角度来看,为了实现的简洁明了,我肯定会选择 lazy val
。但是,如果您从 Scala 集合库作者的角度来看它,事情就会开始有所不同。以这种方式思考,您正在创建一个库,它可能会被许多人使用,并且 运行 在世界各地的许多机器上使用。这意味着您应该考虑每个结构的内存开销,尤其是当您自己创建这样一个基本数据结构时。
我这样说是因为当你使用 lazy val
时,你会按设计生成一个额外的 Boolean
字段,该字段会标记该值是否已初始化,我假设这就是库作者的意思旨在避免。 JVM 上 Boolean
的大小当然取决于 VM,即使是一个字节也是需要考虑的事情,尤其是当人们生成大量 Stream
数据时。同样,这绝对 不是 我通常会考虑的事情,并且绝对是对内存使用的微优化。
我认为性能是这里的关键点之一的原因是 SI-7266 它修复了 Stream 中的内存泄漏。请注意跟踪字节码以确保在生成的 class.
中没有保留额外值的重要性。实现上的区别在于tail
初始化与否的定义是检查生成器的方法实现:
def tailDefined: Boolean = tlGen eq null
而不是 class 上的字段。