资源获取即初始化

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 用户数据的帮助)定义自己的 __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] 的构造在 Lua 中。这个想法是为变量类(如 local)命名为 scoped,当提供一个函数(或可调用表)时,它会在范围退出时调用它

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

可以在纯 Lua 中实现这一点。这在 Lua Programming Gems 中的 Gem #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 的特殊用法。)

“异常安全编程”中的示例 [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

尝试/最终/范围保护补丁

已经发布了一些针对 Lua 的补丁来处理这种类型的事情

(2008-01-31) 补丁:由 Hu Qiwei 发布的 try/catch/finally 支持 [10][11][12]。在 try 块中禁止 returnbreak

(2008-01-07) 补丁:由 Nodir Temirhodzhaev 发布的 对象的最终化[13] 这是上述“try/catch/finally”异常处理机制的替代方案,并且与 [3]资源获取即初始化 相关。

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

(2008-02-12) 补丁:由 Alex Mania 发布的 实验性 finalize/guard [14] 支持 finalizeguard 块以实现 RAII。

LuaList:2009-03/msg00418.html

with 语句 (Metalua, Python)

MetaLua 0.4 提供了一个名为“withdo”的 RAII 扩展。它适用于所有通过调用方法 :close() 释放的资源。它可以防止受保护块的正常终止、从块中返回以及错误。以下代码将在关闭文件句柄 *后* 返回文件名 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] 它可能是两倍或更多倍,具体取决于用户数据的交织程度,但肯定需要两次收集才能摆脱具有 __gc 元数据的用户数据,如果用户数据是对某个其他对象的最后一个引用,而该对象本身是/引用了用户数据,那么循环将继续。(由 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

另请参见


最近更改 · 偏好设置
编辑 · 历史记录
最后编辑于 2012 年 3 月 13 日凌晨 4:36 GMT (差异)