Lua嵌套协程

4
我正在尝试在copas中使用redis-lua库,但需要进行一些修补。其中一个问题是redis-lua将一些迭代器定义为协程,但这些迭代器执行可以yield的网络操作。
因此,coroutine.yield用于两个非常不同的事情:用于迭代器和copas。由于网络调用嵌套在迭代器中,网络yield被迭代器的coroutine.wrap拦截,而不是被copas拦截。
下面的示例显示了问题:
local function iterator ()
  for i = 1, 2 do
    if i == 2 then coroutine.yield () end -- network yield
    coroutine.yield () -- iterator yield
  end
end
local citerator = coroutine.wrap (iterator)

local function loop () -- use of the iterator within a copas thread
  while citerator () do end
end
local cloop = coroutine.create (loop)

while coroutine.resume (cloop) do end -- same as copas loop, executes the cloop thread

是否有一种“标准”解决方案来解决这个问题,同时仍然允许使用协程作为迭代器?

我能够通过“标记”yield(见下文)使一个小例子工作,但它与现有代码不兼容。我可以保持copas代码不变,但必须更新redis-lua中的迭代器。

local function wrap (f, my_tag)
  -- same as coroutine.wrap, but uses my_tag to yield again
  local co = coroutine.create (f)
  return function ()
    local t = table.pack (coroutine.resume (co))
    local code = t [1]
    local tag  = t [2]
    table.remove (t, 1)
    table.remove (t, 1)
    if tag == nil then
      return
    elseif my_tag == tag then
      return table.unpack (t)
    else
      coroutine.yield (tag, table.unpack (t))
    end
  end
end

local Iterator = {} -- tag for iterator yields
local Network  = {} -- tag for network yields

local function iterator ()
  for i = 1, 2 do
    if i == 2 then coroutine.yield (Network, i) end
    coroutine.yield (Iterator, i)
  end
end

local citerator = wrap (iterator, Iterator)

local function loop ()
  while citerator () do end
end

local cloop = wrap (loop, Network)

while cloop () do end

有更好的解决方案吗?

2个回答

5
Lua协程总是返回到它们上次恢复的线程。Copas套接字函数期望返回到Copas事件循环,但实际上它们会被用于实现redis-lua迭代器的协程所阻塞。不幸的是,你无法做太多来解决这个问题,除非改变redis-lua迭代器的代码。之所以还没有人这样做,是因为在Lua 5.2之前(LuaJIT也可以),甚至无法从迭代器函数中yield(redis-lua中的迭代器yield正常工作,因为它们永远不会离开迭代器函数,但你不能像Copas套接字函数那样超出for循环而yield)。
你提出使用标记值来区分迭代器yield与其他yield的想法是好的。你只需要确保将所有不属于迭代器函数的yield传递给协程的上一级,包括任何coroutine.yield和coroutine.resume的参数/返回值(当调用coroutine.wrap函数时,后者是隐式的)。
更具体地说,如果你在redis-lua中有以下代码:
-- ...
return coroutine.wrap( function()
  -- ...
  while true do
    -- ...
    coroutine.yield( some_values )
  end
end )

你需要将它更改为:

-- ...
local co_func = coroutine.wrap( function()
  -- ...
  while true do
    -- ...
    coroutine.yield( ITERATOR_TAG, some_values ) -- mark all iterator yields
  end
  return ITERATOR_TAG -- returns are also intended for the iterator
end )
return function()
  return pass_yields( co_func, co_func() ) -- initial resume of the iterator
end
ITERATOR_TAGpass_yields 函数需要放在 redis.lua 文件的顶部附近:
local ITERATOR_TAG = {} -- unique value to mark yields/returns

local function pass_yields( co_func, ... )
  if ... == ITERATOR_TAG then -- yield (or return) intended for iterator?
    return select( 2, ... ) -- strip the ITERATOR_TAG from results and return
  else
    -- pass other yields/resumes back and forth until we hit another iterator
    -- yield (or return); using tail recursion here instead of a loop makes
    -- handling vararg lists easier.
    return pass_yields( co_func, co_func( coroutine.yield( ... ) ) ) 
  end
end

据我所知,redis-lua开发者计划在年底发布另一个版本,因此他们可能会感激拉请求。

通过您的贡献,我编写了一个小模块,几乎可以透明地替换Lua的coroutine。它通过了lua测试套件。唯一的变化是将协程模块作为以下方式进行引用:local coroutine = require "coroutine.make"() - Alban Linard
@AlbanLinard 不错的想法。我选择了一种不同的方法来自动化这种事情:我编写了一个函数,它可以自动转换使用回调的迭代函数(例如 Lua 5.0 中的旧 table.foreach)为使用协程和标记 yield 的 for 循环迭代器。 - siffiejoe

0
在你的第一个例子中,你使用了 wrap(loop)。我猜这是 copas 的 wrap,因为这段代码中没有提到 copas……
然而,你应该 copas.wrap() 一个 socket,但是你的 loop 是一个函数!
请参阅 copas 文档以获得良好的介绍

我已经编辑了第一个代码清单。wrap实际上是coroutine.create,而对cloop的调用现在是coroutine.resume - Alban Linard

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接