10

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

 3 years ago
source link: https://zhuanlan.zhihu.com/p/337850564
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

深入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,这也是完全合理的。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK