zoukankan      html  css  js  c++  java
  • 《Programming in Lua 3》读书笔记(十三)

    日期: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可能也需要访问这些属性。

  • 相关阅读:
    zzuli 1908
    继承 封装 多态 java的三大特性
    FZU 2232
    zzuli 1079
    zzuli 1023
    二分图的匹配 hdu 1083
    CodeIgniter学习笔记(五)——CI超级对象中的uri
    CodeIgniter学习笔记(四)——CI超级对象中的load装载器
    CodeIgniter学习笔记(三)——CI中的视图
    CodeIgniter学习笔记(二)——CI中的控制器
  • 原文地址:https://www.cnblogs.com/zhong-dev/p/4044572.html
Copyright © 2011-2022 走看看