资源获取即初始化

lua-users home
wiki

本页面将介绍在 Lua 中实现“资源获取即初始化”(RAII) [1] 效果的方法。RAII 是一种非常有用的范例,它在 Lua 5.1 中并未得到直接支持,尽管有一些方法可以近似实现。一些讨论和提出的解决方案可以在 Lua 邮件列表中找到

问题

一个非常典型且适合 RAII 的问题是:

function dostuff()
  local f = assert(io.open("out", "w"))
  domorestuff()  -- this may raise an error
  f:close() -- this is not called if that error was raised
end

dostuff()

如果引发了错误,文件不会立即关闭 (RAII 会确保这一点)。是的,垃圾回收器最终会关闭文件,但我们不知道何时。程序的成功或正确性可能取决于文件锁是否立即被释放。不过,在 pcall 之外显式调用 collectgarbage('collect') 可能会有所帮助,在这种情况下,Lua 会调用 __gc (最终化器) 元方法,该方法会关闭文件,尽管你可能需要调用 collectgarbage 多次 [*1]。此外,Lua 不允许纯 Lua (未借助 C userdata) 实现的对象定义自己的 __gc 元方法。

通过维护一个可析构对象堆栈来模拟 RAII

这是一种纯 Lua 的方法,它维护一个所有需要被回收的对象堆栈。在作用域退出或处理异常时,要回收的对象将从堆栈中移除并进行最终化 (即调用 close,如果存在;否则,将其作为函数调用),以释放其资源。

-- raii.lua
local M = {}

local frame_marker = {} -- unique value delimiting stack frames

local running = coroutine.running

-- Close current stack frame for RAII, releasing all objects.
local function close_frame(stack, e)
  assert(#stack ~= 0, 'RAII stack empty')
  for i=#stack,1,-1 do  -- release in reverse order of acquire
    local v; v, stack[i] = stack[i], nil
    if v == frame_marker then
      break
    else
      -- note: assume finalizer never raises error
      if type(v) == "table" and v.close then
        v:close()
      else
        v(e)
      end
    end
  end
end

local function helper1(stack, ...) close_frame(stack); return ... end
-- Allow self to be used as a function modifier
-- to add RAII support to function.
function M.__call(self, f)
  return function(...)
    local stack, co = self, running()
    if co then  -- each coroutine gets its own stack
      stack = self[co]
      if not stack then
        stack = {}
        self[co] = stack
      end
    end
    stack[#stack+1] = frame_marker -- new frame
    return helper1(stack, f(...))
  end
end

-- Show variables in all stack frames.
function M.__tostring(self)
  local stack, co = self, running()
  if co then stack = stack[co] end
  local ss = {}
  local level = 0
  for i,val in ipairs(stack) do
    if val == frame_marker then
      level = level + 1
    else
      ss[#ss+1] = string.format('[%s][%d] %s', tostring(co), level, tostring(val))
    end
  end
  return table.concat(ss, '\n')
end

local function helper2(stack, level, ok, ...)
  local e; if not ok then e = select(1, ...) end
  while #stack > level do close_frame(stack, e) end
  return ...
end

-- Construct new RAII stack set.
function M.new()
  local self = setmetatable({}, M)

  -- Register new resource(s), preserving order of registration.
  function self.scoped(...)
    local stack, co = self, running()
    if co then stack = stack[co] end
    for n=1,select('#', ...) do
      stack[#stack+1] = select(n, ...)
    end
    return ...
  end

  -- a variant of pcall
  -- that ensures the RAII stack is unwound.
  function self.pcall(f, ...)
    local stack, co = self, running()
    if co then stack = stack[co] end
    local level = #stack
    return helper2(stack, level, pcall(f, ...))
  end

  -- Note: it's somewhat convenient having scoped and pcall be
  -- closures....  local scoped = raii.scoped

  return self
end

-- singleton.
local raii = M.new()

return raii

用法示例

local raii = require "raii"
local scoped, pcall = raii.scoped, raii.pcall

-- Define some resource type for testing.
-- In practice, this is a resource we acquire and
-- release (e.g. a file, database handle, Win32 handle, etc.).
local Resource = {}; do
  Resource.__index = Resource
  function Resource:__tostring() return self.name end
  function Resource.open(name)
    local self = setmetatable({name=name}, Resource)
    print("open", name)
    return self
  end
  function Resource:close() print("close", self.name) end
  function Resource:foo()   print("hello", self.name) end
end

local test3 = raii(function()
  local f = scoped(Resource.open('D'))
  f:foo()
  print(raii)
  error("opps")
end)

local test2 = raii(function()
  scoped(function(e) print("leaving", e) end)
  local f = scoped(Resource.open('C'))
  test3(st)
end)

local test1 = raii(function()
  local g1 = scoped(Resource.open('A'))
  local g2 = scoped(Resource.open('B'))
  print(pcall(test2))
end)

test1()


--[[ OUTPUT:
open    A
open    B
open    C
open    D
hello   D
[nil][1] A
[nil][1] B
[nil][2] function: 0x68a818
[nil][2] C
[nil][3] D
close   D
close   C
leaving complex2.lua:23: opps
complex2.lua:23: opps
close   B
close   A
]]

使用协程的示例

local raii = require "raii"
local scoped, pcall = raii.scoped, raii.pcall

-- Define some resource type for testing.
-- In practice, this is a resource we acquire and
-- release (e.g. a file, database handle, Win32 handle, etc.).
local Resource = {}; do
  Resource.__index = Resource
  local running = coroutine.running
  function Resource:__tostring() return self.name end
  function Resource.open(name)
    local self = setmetatable({name=name}, Resource)
    print(running(), "open", self.name)
    return self
  end
  function Resource:close() print(running(), "close", self.name) end
  function Resource:foo()   print(running(), "hello", self.name) end
end

local test3 = raii(function(n)
  local f = scoped(Resource.open('D' .. n))
  f:foo()
  print(raii)
  error("opps")
end)

local test2 = raii(function(n)
  scoped(function(e) print(coroutine.running(), "leaving", e) end)
  local f = scoped(Resource.open('C' .. n))
  test3(n)
end)

local test1 = raii(function(n)
  local g1 = scoped(Resource.open('A' .. n))
  coroutine.yield()
  local g2 = scoped(Resource.open('B' .. n))
  coroutine.yield()
  print(coroutine.running(), pcall(test2, n))
  coroutine.yield()
end)

local cos = {coroutine.create(test1), coroutine.create(test1)}
while true do
  local is_done = true
  for n=1,#cos do
    if coroutine.status(cos[n]) ~= "dead" then
      coroutine.resume(cos[n], n)
      is_done = false
    end
  end
  if is_done then break end
end
-- Note: all coroutines must terminate for RAII to work.

--[[ OUTPUT:
thread: 0x68a7f0        open    A1
thread: 0x68ac10        open    A2
thread: 0x68a7f0        open    B1
thread: 0x68ac10        open    B2
thread: 0x68a7f0        open    C1
thread: 0x68a7f0        open    D1
thread: 0x68a7f0        hello   D1
[thread: 0x68a7f0][1] A1
[thread: 0x68a7f0][1] B1
[thread: 0x68a7f0][2] function: 0x68ada0
[thread: 0x68a7f0][2] C1
[thread: 0x68a7f0][3] D1
thread: 0x68a7f0        close   D1
thread: 0x68a7f0        close   C1
thread: 0x68a7f0        leaving complex3.lua:24: opps
thread: 0x68a7f0        complex3.lua:24: opps
thread: 0x68ac10        open    C2
thread: 0x68ac10        open    D2
thread: 0x68ac10        hello   D2
[thread: 0x68ac10][1] A2
[thread: 0x68ac10][1] B2
[thread: 0x68ac10][2] function: 0x684258
[thread: 0x68ac10][2] C2
[thread: 0x68ac10][3] D2
thread: 0x68ac10        close   D2
thread: 0x68ac10        close   C2
thread: 0x68ac10        leaving complex3.lua:24: opps
thread: 0x68ac10        complex3.lua:24: opps
thread: 0x68a7f0        close   B1
thread: 0x68a7f0        close   A1
thread: 0x68ac10        close   B2
thread: 0x68ac10        close   A2
]]

--DavidManura

作用域管理器

JohnBelmonteLuaList:2007-05/msg00354.html [*2] 中建议实现类似于 D 语言的 scope 卫语句 [3][4] 的构造。想法是创建一个名为 scoped 的变量类 (如 local),当提供一个函数 (或可调用的表) 时,它将在作用域退出时调用该函数。

function test()
  local fh = io:open()
  scoped function() fh:close() end
  foo()
end

这可以用纯 Lua 实现。这在 Lua Programming Gems 的第 13 篇 "Lua 中的异常" [5] 中进行了描述,允许以下类似的操作:

function dostuff()
  scope(function()
    local fh1 = assert(io.open('file1'))
    on_exit(function() fh1:close() end)
    ...
    local fh2 = assert(io.open('file2'))
    on_exit(function() fh2:close() end)
    ...
  end)
end

作用域卫语句的可能语法扩展

这需要构造一个匿名函数,但从效率角度来看,避免这一点是有好处的。

这是另一个非常基础的想法 ("finally ... end" 构造):

function load(filename)
   local h = io.open (filename)
   finally if h then h:close() end end
   ...
end

请注意,D 语言中实现的 scope 构造在语法上类似于在作用域结束时执行的 if 语句。也就是说,如果我们认为 exitsuccessfailure 是真实的条件表达式;事实上,将这种泛化扩展可能是有用的。我曾提出以下 Lua 语法扩展:

stat :: scopeif exp then block {elseif exp then block} [else block] end

其中 err 是一个隐式变量 (类似于 self),可以在 expblock 中使用,表示正在引发的错误,或者在没有引发错误时为 nil。(注释:在许多个月后重新审视该语法时,我发现其语义并不直观,尤其是在 err 的特殊用法方面。)

"Exception Safe Programming" [3] 中的示例翻译成 Lua 如下:

function abc()
  local f = dofoo();
  scopeif err then dofoo_undo(f) end

  local b = dobar();
  scopeif err then dobar_undo(b) end

  local d = dodef();

  return Transaction(f, b, d)
end
-----
function bar()
  local verbose_save = verbose
  verbose = false
  scopeif true then verbose = verbose_save end

  ...lots of code...
end
-----
function send(msg)
  do
    local origTitle = msg.Title()
    scopeif true then msg.SetTitle(origTitle) end
    msg.SetTitle("[Sending] " .. origTitle)
    Copy(msg, "Sent")
  end
  scopeif err then
    Remove(msg.ID(), "Sent")
  else
    SetTitle(msg.ID(), "Sent", msg.Title)
  end
  SmtpSend(msg)	-- do the least reliable part last
end

scopeif true then ... end 有点冗长,尽管与 while true do ... end 类似。使用 scopeif 而不是 scope if 遵循了 elseif 的模式。

JohnBelmonte 的数据库示例被缩短为:

function Database:commit()
  for attempt = 1, MAX_TRIES do
    scopeif instance_of(err, DatabaseConflictError) then
      if attempt < MAX_TRIES then
        log('Database conflict (attempt '..attempt..')')
      else
        error('Commit failed after '..attempt..' tries.')
      end
    end -- note: else no-op
    self.commit()
    return
  end
end

这是如何模拟常规 RAII 的方式 (D 语言文章并未说明 RAII 永无用处)。

function test()
  local resource = Resource(); scope if true then resource:close() end
  foo()
end

然而,这比建议的要冗长。

function test()
  scoped resource = Resource()
  foo()
end

也许这可以在 Metalua [6] 中进行原型化。

--DavidManura

Try/finally/scoped-guard 补丁

Lua 已经发布了一些补丁来处理这类事情。

(2008-01-31) 补丁:Hu Qiwei 提交了 **try/catch/finally** 支持 [10][11][12]。在 try 块中禁止使用 returnbreak

(2008-01-07) 补丁:Nodir Temirhodzhaev 提交了 **对象最终化** [13]。这是上述 "try/catch/finally" 异常处理机制的一种替代方案,并且与 [3]ResourceAcquisitionIsInitialization 相关。

更新 2009-02-14:LuaList:2009-02/msg00258.htmlLuaList:2009-03/msg00418.html

(2008-02-12) 补丁:Alex Mania 提交了 **实验性 finalize/guard** [14],支持用于 RAII 的 finalizeguard 块。

LuaList:2009-03/msg00418.html

with 语句 (MetaLua, Python)

MetaLua 0.4 提供了一个名为 "withdo" 的 RAII 扩展。它适用于所有通过调用 :close() 方法释放的资源。它能防止块的正常终止、块内的 return 和错误。下面的代码将在关闭文件句柄 *之后* 返回 filename1 和 filename2 的文件大小之和。

with h1, h2 = io.open 'filename1', io.open 'filename2' do
   local total = #h1:read'*a' + #h2:read'*a'
   return total
end

需要注意的是,MetaLua 的设计受限于资源对象必须具有特定方法 ("close()" 在本例中) 的要求。在 Python 中,这被 "with ... as ..." 语法所取代,后者允许使用独立于资源的资源管理对象 [7]。此外,Python 语句允许省略赋值,因为在许多情况下不需要资源变量--例如,如果你只想在块内保持一个锁。

附加评论

[*1] 它可能是两次或更多次,这取决于 userdata 的交织程度,但要摆脱一个带有 __gc 元方法的 userdata,肯定需要两次垃圾回收。如果 userdata 是另一个对象的最后一个引用,而该对象本身是/引用了一个 userdata,那么循环就会继续。(由 RiciLake 记录)

[*2] RAII 模式的缺点是需要创建临时类来管理资源,以及在连续获取资源时所需的笨拙嵌套。请参阅 http://www.digitalmars.com/d/exception-safe.html。一种更好的模式是在 Lua gem #13 "Lua 中的异常" [5] 中。--JohnBelmonte

这是我提出的示例代码,说明了类似于 Google Go 的 defer 和 D 语言的 scope(exit) 的语法。--DavidManura

-- Lua 5.1, example without exceptions
local function readfile(filename)
  local fh, err = io.open(filename)
  if not fh then return false, err end
  local data, err = fh:read'*a'
  -- note: in this case, the two fh:close()'s may be moved here, but in general that is not possible
  if not data then fh:close(); return false, err end
  fh:close()
  return data
end
-- Lua 5.1, example with exceptions, under suitable definitions of given functions.
local function readfile(filename)
  return scoped(function(onexit)  -- based on pcall
    local fh = assert(io.open(filename)); onexit(function() fh:close() end)
    return assert(fh:read'*a')
  end)
end
-- proposal, example without exceptions
local function readfile(filename)
  local fh, err = io.open(filename); if not fh then return false, err end
  defer fh:close()
  local data, err = fh:read'*a'; if not data then return false, err end
  return data
end
  -- note: "local val, err = io.open(filename); if not val then return false, err end" is a common
  -- pattern and perhaps warrants a syntax like "local val = returnunless io.open(filename)".
-- proposal, example with exceptions
local function readfile(filename)
  local fh = assert(io.open(filename)); defer fh:close()
  return assert(fh:read'*a')
end
-- proposal, example catching exceptions
do
  defer if class(err) == 'FileError' then
    print(err)
    err:suppress()
  end
  print(readfile("test.txt"))
end

-- alternate proposal - cleanup code by metamechanism
local function readfile(filename)
  scoped fh = assert(io.open(filename)) -- note: fh:close() or getmetatable(fh).__close(fh) called on scope exit
  return assert(fh:read'*a')
end

另请参阅


RecentChanges · preferences
编辑 · 历史
最后编辑时间 2012年3月12日 晚上11:36 GMT (差异)