深入Lua:在C代码中处理协程Yield

在Lua和C的交互中,Lua经常调用C来完成一些性能敏感的操作,有时候C也会反过来回调Lua层的代码,比如 table.sort
这个API。

假设我们要用C来实现一个 foreach
函数用于遍历table,并在遍历过程中回调Lua的函数,让Lua能够访问每个key和value。

Lua层像这样使用这个函数:

local mylib = require "mylib"
local t = {x = 1, y = 23, name = "jim"}
mylib.foreach(t, print)

预期的输出是:

name    jim
x       1
y       23

这个函数的C代码如下:

static int l_foreach(lua_State *L) {
luaL_checktype(L, 1, LUA_TTABLE);
luaL_checktype(L, 2, LUA_TFUNCTION);
lua_pushnil(L);                     // table | fun | nil
while (lua_next(L, 1) != 0) {       // table | fun | key | value
lua_pushvalue(L, 2);            // table | fun | key | value | fun
lua_pushvalue(L, -3);           // table | fun | key | value | fun |key
lua_pushvalue(L, -3);           // table | fun | key | value | fun |key | value
lua_call(L, 2, 0);              // table | fun | key | value
lua_pop(L, 1);                  // table | fun | key
}
return 0;
}

foreach正常情况下执行得很好,但有一种情况却有问题:就是Lua的回调函数在一个协程中,且调用了coroutine.yield时,比如:

local mylib = require "mylib"
local t = {x = 1, y = 23, name = "jim"}
--创建一个协程
local co = coroutine.wrap(function()
mylib.foreach(t, function(k, v)
coroutine.yield(k, v)
end)
end)
--让协程去取table的值返回
while true do
local k, v = co()
if k then
print(k, v)
else
break
end
end

运行这段代码会得到这个错误: attempt to yield across a C-call boundary

原因是Lua使用longjmp来实现协程的挂起,longjmp会跳到其他地方去执行,使得后面的C代码被中断。 l_foreach
函数执行到lua_call,由于longjmp会使得后面的指令没机会再执行,就像这个函数突然消失了一样,这肯定会引起不可预知的后果,所以Lua不允许这种情况发生,它在调用coroutine.yield时抛出上面的错误。

那我们有没有办法让回调函数可以yield呢?在Lua5.1以前是不可以的,Lua5.2之后提供了几个新的API,允许回调的Lua函数可以yield,这几个API是: lua_yieldk, lua_callk, lua_pcallk
,像lua_callk的声明如下:

void lua_callk (lua_State *L,
int nargs,
int nresults,
lua_KContext ctx,
lua_KFunction k);

其中最重要的是k参数,这是一个称为 continuation function
的回调函数。它的含义是lua_callk回调了函数,函数调用了yield使协程挂起,接着当协程恢复执行权回到C层时,Lua会调用这个k函数。那这个k函数的作用就很清楚了:延续lua_callk后面的逻辑。k函数的原型如下:

typedef int (*lua_KFunction) (lua_State *L, int status, lua_KContext ctx);

status表示调用该函数时的状态,比如 LUA_YIELD
表明协程是从yield恢复回来的。

ctx就是lua_callk传入的那个参数,没有特别的含义,由调用者自己解释,它是一个可以容纳指针的整型。

有了lua_callk,我们就可以改造foreach函数,使其支持yield:

static int finishcall(lua_State *L, int status, lua_KContext ctx) {
if (status == LUA_OK)
lua_pushnil(L);
else    // LUA_YIELD
lua_pop(L, 1);
while (lua_next(L, 1) != 0) {
lua_pushvalue(L, 2);
lua_pushvalue(L, -3);
lua_pushvalue(L, -3);
lua_callk(L, 2, 0, 0, finishcall);
lua_pop(L, 1);
}
return 0;
}
static int l_foreach(lua_State* L) {
luaL_checktype(L, 1, LUA_TTABLE);
luaL_checktype(L, 2, LUA_TFUNCTION);
return finishcall(L, LUA_OK, 0);
}

具体的逻辑都移到finishcall去了,它有两种状态:

  • LUA_OK 是l_foreach主动传入的,相当于初始执行状态,在这里lua_pushnil开始遍历table。
  • LUA_YIELD 是Lua底层回调过来的,表明它所在的Lua协程从yield恢复了执行权。lua_calk后面的代码在yield之后没有机会执行,所以这个分支要做的是恢复后面的代码(lua_pop),然后继续循环。

我们再次执行上面的Lua代码,就能看到下面的输出:

y       23
x       1
name    jim

在协程里,可以通过 coroutine.isyieldable
判断协程是否可以yield。

虽然可以用延续函数来实现回调函数的yield。但这不是万灵药,有些C函数需要很多上下文才能支持代码的延续,比如像table.sort这种严重依赖于递归的函数。为了支持yield而把代码搞得很复杂,而且还丢失了一些性能,这明显有些得不偿失。所以Lua API很多回调函数都不支持yield,这也是完全合理的。

参考资料

lua in-depth
我还没有学会写个人说明!
上一篇

Abp VNext 入门——让ABP跑起来

下一篇

亚马逊云如何重塑世界?

你也可能喜欢

评论已经被关闭。

插入图片