协程,简单来说就是新创建一个协助程序(co = coroutine.create(func)),然后需要手动去启动它(coroutine.resume(co)),在它最终退出之前,它有可能暂停多次返回阶段性的结果(coroutine.yield(co)),每一次暂停之后都必须手动去恢复它(coroutine.resume(co))。
协程在lua源文件中对应lcorolib.c,数组co_funcs中定义了c暴露给lua的接口。从上面的描述看和c函数调用有点相似,只不过c函数只有一个出口,所以不可能返回多次。题外话,为什么c函数只有一个出口?我自己粗浅的理解是因为c函数的所有信息都放在栈上,而c语言没有提供原生的保存/恢复栈空间的支持,所以没有中途退出后还能生新进入这个概念。实际上,协程和系统级别的进程切换更像一点,都是保存堆栈,然后恢复。我想最大的不同就是协程知道接下来的控制权在哪里,而进程不知道。根本上它们想实现的功能就不一样吧。
好了,那协程实现的要点就是堆栈的保存与恢复了。当然,这里的堆栈不是进程本身的堆栈,而是lua的soft stack。从代码上来说吧:
82 static int luaB_cocreate (lua_State *L) { 83 lua_State *NL; 84 luaL_checktype(L, 1, LUA_TFUNCTION); 85 NL = lua_newthread(L); 86 lua_pushvalue(L, 1); /* move function to top */ 87 lua_xmove(L, NL, 1); /* move function from L to NL */ 88 return 1; 89 }
其中NL就是新创建的协程的栈,以后所有的保存/恢复都是针对这个栈。lua_State这个结构体里对协程实现最重要的是CallInfo *ci,CallInfo的定义如下:
66 /* 67 ** information about a call 68 */ 69 typedef struct CallInfo { 70 StkId func; /* function index in the stack */ 71 StkId top; /* top for this function */ 72 struct CallInfo *previous, *next; /* dynamic call link */ 73 short nresults; /* expected number of results from this function */ 74 lu_byte callstatus; 75 ptrdiff_t extra; 76 union { 77 struct { /* only for Lua functions */ 78 StkId base; /* base for this function */ 79 const Instruction *savedpc; 80 } l; 81 struct { /* only for C functions */ 82 int ctx; /* context info. in case of yields */ 83 lua_CFunction k; /* continuation in case of yields */ 84 ptrdiff_t old_errfunc; 85 lu_byte old_allowhook; 86 lu_byte status; 87 } c; 88 } u; 89 } CallInfo;
其中func指向当前调用的函数在栈上的位置,而savedpc就是保存的指令执行位置(先无视union里的c),根据这两个值就能恢复函数的执行点。然而在yield的时候真正负责保存函数位置的是extra(保存func与栈顶的相对位置),在resume时func会根据extra来恢复,有没有这个需要我是表示怀疑的,因为就算resume传递的参数导致栈realloc,使func失效,但在luaD_reallocstack内会调用correctstack将调用链上所有的func重新设置为正确的值,所以这里是不是多余的呢?
在lua 5.2中调用路径包含c函数的时候也能够进行yield,只不过不甚好看。由于c函数不能保存堆栈,所以lua的策略是直接放弃当前c函数的栈幀,而让调用者本身提供一个continuation,当resume时调用上面被无视的uion里的c.k。没用过,所以也不深入考究了。