日期:2014.7.16
PartⅡ 17
Weak Tables and Finalizers
Lua实现的是自动的内存管理。程序可以创建对象,可是没有现成的函数来实现删除对象。Lua使用 garbage collection(垃圾回收机制?)来删除变成gargage的对象,这一特性带来了很大的便利,不再深陷于内存回收,并且可以避免很多因为内存回收而引发的一系列问题,如悬垂指针和内存泄漏。
本章节提到的Weak Tables和Finalizers是lua提供的一个特性,允许用户参与到lua的garbage collector机制中。Weak Table允许回收程序依旧在使用的对象,而finalizer则允许回收garbage collector没有完全或者说直接控制到的对象。
17.1 Weak Tables
garbage collector只会回收那些确定为garbage的对象,但是它推断不出用户认为哪些变量是garbage。lua中任何全局变量都不会是garbage,尽管程序没有再使用过这些变量Lua也不会自动回收。这也是这本书开篇讲到过的,全局变量在不使用的时候赋值为nil,这时系统才会自动回收内存。因此说Lua是推断不出用户的主观行为。
有的时候仅仅是清除了相关的引用是不够的,when you want to keep a collection of all live objects of some kind in your program.This task seems simple:all you have to do is to insert each new object into the collection.However ,once the object is part
of the collection,it will never be collected.这段的意思到底是什么呢?我们需要做的仅是将新的对象插入到collection中,但是一旦我们这些对象成为了collection的一部分便再也不会被collected了。Lua并不知道这样的引用不能去阻止Lua对对象的回收,除非用户告诉Lua?
Weak table就是用来告知Lua某个引用不能去阻止Lua对某个对象的回收的机制。一个weak reference就是对一个garbage collector没有管理的对象的引用。如果所有指向某个对象的引用是weak的,那么这些对象将会被回收且这些引用也会被系统删除。weak table就是lua用来实现weak reference的,该table里面的存储的都是weak的。这就意味着,如果某个对象仅是受weak table控制,那么lua最终会回收这个对象。
Table拥有key和value,这两个值都可以是任何类型的对象。在一般情况下,garbage collector不会回收作为table的key或者value的对象,这就是说table的key和value都是强引用(strong reference),这就会影响lua回收他们所指向的对象。而在weak table中key和value都可以是weak的,这就意味着weak table会有三种类型:key是weak而value不是;value是weak的而key不是;key和value都是weak的。不管weak
table是何种类型,只要key或者value被回收了那么整个table里面的内容都会被回收掉。
涉及到元表。元方法 __mode 赋予了table的弱特性,该方法的值类型为string类型。当值为"k",表示key是weak的;当值为"v",表示value是weak的;当值为"kv",则表示key和value都是weak的。
e.g. a = {} --此时表示key为weak b = { __mode = "k"} setmetatable(a,b) key = {} a[key] = 1 key = {} a[key] = 2 collectgarbage() for k,v in pairs(a) do print(v) end
在这个例子中,__mode的值是"k",表明这个table以key为weak,具体的例子中,对key的第二次赋值重写了key第一次复制时的引用,所以但进行内存回收时将第一次赋值的key回收了,而第二次赋值的没有。。。这里的key是一个table,是一个对象所以可以被回收。
要注意的是只能从weak table回收对象,而对于如numbers和booleans等变量,则不能回收。即假如我们以一个number作为tablekey,那么collector将不会移除这个key,当然当table的value是weak的,不管key是否是weak亦不管key的类型是不是对象,当value被回收了整个table里面的元素都会被移除了。
如果key是string类型这里需要特殊考虑:尽管string是可回收的,从实现角度看,其不像其余可回收的对象。像table和thread都是明确的创建的,如我们写a = {},就明确的创建了一个table。然而,"a" .. "b" 此时会创建一个string型变量嘛?假如此时系统中已经存在一个"ab"了怎么办?lua会继续创建一个嘛?编译器会在运行程序前就创建一个string型变量嘛?从程序员角度来看,string是变量而不是对象。因此,和number或boolean一样,string也不会从weak
table中移除(除非value是weak的)。
17.2 Memoize Functions
记忆函数?
A common programming technique is to trade space for time?啥意思??(用空间换取时间??)能通过记住该函数的运算结果进而提升一个函数的运行效率,效率体现在当用同一个参数调用该函数的时候,直接返回已经记住的结果。这应该就是前文讨论的模块的设计思路--同一个模块一般情况下只会加载一次。
e.g. local results = {} function mem_loadstring( s ) local res = results[s] --从table中访问该参数 if res == nil then --如果该table中没有该值 res = assert(load(s)) results[s] = res --将该值存入table中,下次访问的时候直接返回该值 end return res end
书上提到了存储这些可能会占用很大的空间,但是带来了效率的提升。这差不多是trade space for time 这句话的解释吧。有的时候,这也会带来不必要的浪费,比如说有些时候可能会以同一个参数频繁的调用某个函数,但是某些时候仅仅会调用一次,假如一直存储着这些信息这样就带来了不必要的浪费。一般情况下,上例中的results会累积存储每次以新参数调用该函数的信息,这样下去总会在某个时间点耗尽系统内存。此时上文提到的weak table就提供了解决方案。如果resultes中有weak的value,那么每次garbage-collection的回收就会移除在该回收点没有使用的value,这也意味着该results里面存储的信息都会被释放掉。
e.g. local results = { } --表示此时table中的value是weak的 setmetatable(results,{__mode = "v"}) function mem_loadstring( s ) <同上> end
因为函数的参数是string型的(table的key),所以我们可以考虑将table设置为true weak的
e.g. setmetatable(results,{__mode = "kv"})
结果是一样的。
这个机制也适合在我们想让某些对象是唯一值的情况。例如,用table表示颜色的时候,有三个字段red,green,blue,通常我们会这样创建
e.g. function createRGB( r,g,b ) return {red = r,green = g,blue = b} end
在引入了我们现在讨论的这个机制后:
e.g. local results = {} setmetatable(results,{__mode = "v"}) function createRGB( r,g,b ) local key = r .. "-" .. g .."-" b --保持key的唯一性 local color = results[key] if color == nil then color = {red = r,green = g,blue = b} results[key] = color end return color end
这样就保证每次以同参数创建的table都是同一个。引入了这一机制后,用户也可以直接比较通过两个color了,假如是同参数创建的那么就是同一个table,此时比较是相等的。否则就一定是不相等的。
17.3 Object Attributes
对象属性
另一个使用到了weak table的地方是将对象与其属性向关联起来。很多时候我们都需要将一些属性附加至对象上:函数的名字,table的默认值,数组的大小等等。
当对象是table的时候,我们可以将这些属性以一个特殊的key存储在自身这个table里面。如我们前文所采用的,最简单又是最唯一的key就是创建一个新的对象(通常是一个table)。但是当对象不是table的时候,这些属性就不能存储在自身,这个时候我们就需要采取别的方法来实现我们的要求了。
用额外的一个table使对象与其属性绑定起来,以对象为key,其属性为value。这个table将保存所有类型对象的属性,这也带来了困扰---不能回收这些对象了,因为这些对象被以key来使用。此时我们就需要引入weak key机制,使用weak key是考虑到,使用weak key不会影响系统回收那些没有被引用的对象;而从另一方面来考虑,如果是使用weak value,一旦value被回收了,与之相关联的对象也会被回收,这是我们不期望的。
17.4 Revisiting Tables with Default Values
我们已经讨论过如何实现创建一个带默认值的table。现在以我们在讨论的weak table来回顾一番这个主题。这里将会涉及到两个解决方案:object attributes and memorizing。
首先第一个方案:使用weak table来绑定table和它的默认值:
local defaults = {} --设置defaults的key为weak setmetatable(defaults,{__mode = "k"}) --在访问table元素的时候,如果没有该key则返回defaults的值,这里的参数是table,保持唯一性 local mt = {__index = function ( t ) return defaults[t] end} --设置table的默认值,以table本身为defaults这个table的key function setDefault( t,d ) defaults[t] = d setmetatable(t,mt) end
这里如果defaults没有设置weak key,那么该table会将z在程序运行期间永久保存所有table的默认值。
此时假如我们这样操作:
e.g. local a = {} setDefault(a,1) --那么我们访问一个a中不存在的元素 print(a.x) --1 使用其默认值。
第二个方案:
e.g. local metas = {} --这里weak table设置value为weak的 setmetatable(metas,{__mode = "v"}) function setDefault( t,d ) --每次从访问这个weak table,看是否有这个默认值的table local mt = metas[d] if mt == nil then --如果没有则创建table作为t的元表 mt = { __index = function ( ) return d end} --以默认值为key保存这个元表 metas[d] = mt end --设置t的元表,带默认参数d setmetatable(t,mt) end
在这里我们为每个不同的默认值设置不同的元表,但是我们会在每次使用同一个默认参数的时候复用同一个元表。
这里将value设置为weak的主要是为了能回收这些没有用到的元表。
针对不同情况,这两种方案有不同的性能表现。第一个方案需要为每个不同默认值的table准备内存空间(存储这些默认值);第二个方案则为不同的默认值准备空间(该方案以是否默认值不同而来设计的,即假如多个table共用一个默认值,那么此时只会存储一个值)。因此当我们的程序有数千个table但是只需要准备少数几个默认值,那么适合使用第二套方案;而如果table较少,所用的默认值也少,那么就适合使用第一套方案。
17.5 Ephemeron Tables
蜉蝣table??
设想这种情况:一个table其key是weak的,而其value又与其key相关联。
这种情况似乎是有可能的。例如,有一个常数函数构造工厂?,该函数接受一个对象参数并返回该对象的一个函数,无论何时访问这个函数都是返回该对象:
e.g. function factory( o ) return function ( ... ) return o end end 使用我们之前讨论的memorizing do local mem = {} setmetatable(mem,{__mode = "k"}) function factory( o ) local res = mem[o] if not res then res = function ( ... ) return o end mem[o] = res end return res end end
这样就不会每次都创建新的函数而增加开销,直接从mem这个weak table中寻找需要的信息。这一段的内容有点让人混淆:该table是key为weak的,而value不是,作者说value不会被collect,因为value是对每个function的强引用(这里指该factory)。之前提到的只要value或者key是weak的,一旦其中一个被collect了,那么该table里的都会被移除掉,书上说的是(whole entry disappears)难道指的是移除而不是被回收吗?
Lua5.2版本中提出了一个概念:ephemeron tables.指的是key是weak的,而value是strong的table。在ephemeron table中,key的可访问性影响着与之相关联的value的可访问性。The reference to v is only strong if there is some strong reference to k.如果对k有强引用那么对v也只能是强引用的,否则就会被移除,即便v直接或间接的引用了k。
17.6 Finalizers
Lua的garbage collector不仅可以用来收集lua的对象,同时也可以用来释放资源。现有多种语言提供finalizer的机制。finalizer指的是一个与一个对象相联系的当该对象要被collected时调用的一个函数:
e.g. o = {x = "hi"} setmetatable(o,{__gc = function (o ) print(o.x) end}) o = nil collectgarbage() --print hi
当我们调用collectgarbage()方法进行回收的时候,调用了与o关联的finalizer。
从上可以看得出,lua实现finalizer是通过设置元方法:__gc来实现的。
需要注意的:在设置元表的时候,需要先设置其元方法,也可以说是在设置元表前先标记对象。这点其实与之前讲元表-元方法的时候类似,如果先设置元表,再定义元方法其实lua是不会去执行我们定义的元方法的。因此假如上例这样实现:
e.g. o = {x = "hi"} mt = {} setmetatable(o,mt) mt.__gc = function (o ) print(o.x) end o = nil collectgarbage() -- 这里不会打印任何东西,还可能引发不可预计的错误
而如果非要在设置完元表再设置元方法,可以先在元表内部给__gc 这个字段赋值(可以是任何类型)再在设置完元表后定义元方法:
e.g. o = {x = "hi"} mt = {__gc = true} setmetatable(o,mt) mt.__gc = function (o ) print(o.x) end o = nil collectgarbage() --hi 这样就能正确打印出来
lua的collector依据标记的顺序来处理一次finalize多个对象的finalizer
e.g. mt = { __gc = function ( o ) print(o[1]) end} list = nil for i=1,3 do list = setmetatable({i,link = list},mt) end list = nil collectgarbage() -- 3 2 1
3是最后被标记的,所以最先被打印出来。
当调用一个finalizer的时候,该函数会调用标记的对象作为自己的参数。而其实此时该对象已经被回收掉了,而在该finalizer的函数体内实现了“复活”,因此在该finalizer结束执行前还是可以访问到作为其参数的对象的。“复活”这一特性是可以传递的:
e.g. A = {x = "this is A"} B = {f = A} setmetatable(B,{__gc = function (o) print(o.f.x) end}) A,B = nil collectgarbage()
这个例子很好的解释了传递这一特性,A已经被回收了,但是并没有设置finalizer,而B的一个value为A,B设置了finalizer。当A,B都被赋值为nil强制回收之后,在B的finalizer内部B实现了“复活”,而该特性传递给了B的valueA,与之相应的A也实现了复活。
因为“复活”这个机制的影响,对象被回收其实要经历两个阶段,第一个阶段回收器会对有finalizer的对象进行确认还没有调用它的finalizer,并“复活”该对象然后执行其finalizer,一旦该finalizer被执行了lua便会标记该对象为已经finalize了。第二个阶段回收器检测到该对象已经被finalize了,就会删除该对象。因此为了确保程序中所有的garbage都被回收了,需要强制调用collectgarbage这个函数两次。
因为lua会标记对象是否已经被finalize,所以对象的finalizer只会调用一次。如果直到程序运行结束某个对象都没有被回收,lua将会在整个lua的state被关闭之前调用其finalizer。
另一个有趣的机制是:可以实现每次当lua完成一个垃圾回收就调用一个给定的函数。这里的实现原理是,尽管finalizer只会实现一次,但是可以在每次执行的时候重新创建一个新的对象去运行下一个finalizer:
e.g. do local mt = {__gc = function ( o ) print("new cycle") setmetatable({},getmetatable(o)) -- 每次执行finalizer就重新创建一个对象设置为同一个元表,同一个元方法 end} setmetatable({},mt) end collectgarbage() collectgarbage() collectgarbage()
而对于拥有finalizer的对象和weak table之间的关系这里也需要讨论一番:回收器会在“复活”之前清理weak table的values,而其key则是在“复活”之后进行清理:
e.g. wk = setmetatable({},{__mode = "k"}) wv = setmetatable({},{__mode = "v"}) o = {} wv[1] = o;wk[o] = 10 setmetatable(o,{__gc = function ( o ) print(wk[o],wv[1]) end}) o = nil collectgarbage() --10 nil print(wk[o]) --nil
以上例子做出来很好的解释。wk其key是weak的,而wv其value是weak的。设置好元表之后,执行回收可以看到,打印出了10而没有打印出wv的元素,因为在垃圾回收之前wv就已经被清理了,而wk在回收之后清理。这也合理的解释了为什么我们使用weak key的table来存储对象的属性,因为设计中可能finalizer可能也需要访问这些属性。