闭包就是一个函数,这个函数可能会访问到函数外部的变量。
简单闭包(就是函数):
def addOne = (i:Int) => 1 + i
访问外部变量的闭包,如 :
var factor = 1
def addOne = (i:Int) => factor + i
以上函数应用到函数外的变量factor,定义这个函数的过程就是将 外部的变量的引用 捕获并构成一个封闭的函数。这样, factor 和 addOne 就构成了一个闭包。
这样相当于factor变量的引用被 “包”在了addOne函数之中, 如果factor变量值改变, 那addOne中factor引用的值也会改变, 若factor重新定义,addOne还是会指向原先的引用。
var factor = 1
def addOne = (i:Int) => factor + i
addOne(1) // 结果为2
factor = 2
addOne(1) //结果为3
var factor = 4
addOne(1) //结果为3
假设,现有以下两个类 :
class Func1{
val n = 2
def addTwo = (i : Int) => i + n
}
class Func2{
def addTwo = (i : Int) => i + 2
}
在Scala中 ,函数也是一个对象, 上例中addTwo就是Function1[Int,Int]的一个对象, 而Function1[Int,Int] 这个类实现了序列化接口, 说明 addTwo 是可被序列化的。
Func1中的 addTwo方法依赖了Func1类的成员变量n,所以 Func1的addTwo方法依赖于Func1类, 然后Func1类是不能被序列化的, 所以这个闭包不能被序列化。
Func2中的addTwo方法不会依赖Func2中的成员变量,所以这个闭包可以被初始化。
为了执行作业,Spark将RDD操作的处理分解为任务,每个任务都由执行程序执行。在执行之前,Spark计算任务的闭包。该闭包需要被序列化并发送给每个执行者。
简单来说, 每个任务是由若干个闭包组成, 任务需要被分配到各个节点上去执行, 这就需要每个闭包都可以被序列化。
接上例, 在spark中分别调用上述方法:
val errorRdd = listRDD.map(new Func1().addTwo) ----1
val rdd = listRDD.map(new Func2().addTwo) ----2
在 1 中, 因为这个闭包不能被初始化, 所以会报错
在2中, 因为new Func2() 对于 addTwo 的闭包来说是无用的,所以在传递的过程中,spark会把这个无用的对象的引用给清理掉。
在许多算子中,都会见到此方法。 作用就是清除一个闭包中的无用依赖,使其准备好序列化并发送到其他执行者。
private[spark] def clean[F <: AnyRef](f: F, checkSerializable: Boolean = true): F = {
ClosureCleaner.clean(f, checkSerializable)
f
}
总结 :
spark对闭包的处理 :
1、减少闭包中无用的引用, 减少闭包序列化后的大小, 有利于网络传输
2、尽可能保证闭包可序列化, 若不行, 则抛出异常: Task not serializable