Lua的协程和 Golang的协程不同,它是在同一个主线程上跑的协程,个人感觉用途不是很大,毕竟没有发挥多核的优势,不过还是有不少人认为这是 Lua的一个亮点,可以用来实现异步代码改写为同步代码,减轻人脑负担,然而很多人用的时候,并不了解当 Lua协程调用到C函数而C函数又调用到Lua函数后又执行 yield
的解决方案。本篇主要是来探讨Lua协程的设计。
Lua协程的设计思路
试想一下,如果你来设计一个在同一个主线程上跑,且没有调度的协程,你会怎么做?
可能你会说这还不简单,我们都已经知道了 CallInfo
这样的结构,只需要创建一个新的Lua栈,将新的 函数设置进其 CallInfo
,当执行到 resume
时,则将 Lua栈 推入,去执行新的指令不就行了?
如果Lua只在自己的世界里面玩,从来不调用 C函数,那就还好。但问题是Lua会与其宿主语言也就是C语言进行打交道,会调用C的函数,如果这个C函数又调用了Lua Function,而其又调用了 yield
,等到它又被 resume
的时候,它就没办法继续执行那尚未执行完成的C函数。
大致执行流程如下
1 | // 因为 lua 的 resume,其实是在C中导出的 |
一种可行的思路是,将Lua的协程与每一个系统线程绑定,消耗高(不过我觉得这样才能发挥出多线程的优势嘛)。
Lua采用的方案则是,通过保存C函数和其状态,并标记状态,当 resume
时根据已有信息,回到原来未执行完C函数的位置。
以下的 lua_pcallk
为使用例子,倒数第二个参数为上下文,倒数第一个参数则是该C函数如果被中断后,应该继续执行的事情。
1 | static int luaB_pcall (lua_State *L) { |
Coroutine
create
先来看创建操作,调用 lua_newthread
创建一个新协程,这里面的协程的状态信息还是 lua_State
,各个协程之间的公共数据则在 global_State
。
lua_xmove
则是将两个 lua_State
的数据转移。
1 | LUA_API void |
resume
创建好协程,还需要手动调用 resume
才能执行,主要依托于 auxresume
,将参数拷贝到协程中,调用 lua_resume
。
1 | static int auxresume (lua_State *L, lua_State *co, int narg) { |
lua_resume 会检查各种条件,包括协程状态,调用层数。
接下来会将 nny
设置为 0,这个 nny 指的是 number of non-yieldable" calls
,它是用来控制是否允许 yield
的,最终会以保护的形式调用 resume
。
1 | LUA_API int lua_resume (lua_State *L, lua_State *from, int nargs) { |
如果是协程刚开始的时候,那就像是执行一个函数那么简单。相反如果是从 yield
状态切换回来,
其实这必然是 C函数中过来的,因为 lua调用 yield
其实还是到了C函数这。
如果在 lua 则继续解析指令即可,这里的 lua 其实是 hook 函数,看起来是 lua 函数 其实还是 C函数,可以看到 之前的堆栈信息存在了 CallInfo->extra
,所以 resume
回来之后,实际上不会有 Lua函数,但是我们要跳过 Lua的指令。
若是在C中 调用的 lua函数,而lua函数又调用了 yield
,则看看 我们之前保存的继续处理函数和上下文存不存在,再去调用即可(调用的是C函数剩余的部分)。
执行完之前遗留的工作以后,只是说恢复到了正确的工作,别忘了 lua 中可能还有要执行的任务,因此会调用 unroll
。
1 | static void resume (lua_State *L, void *ud) { |
unroll
较为简单,执行接下来的字节码,如果是停在了C函数,则会调用 finishCcall
去执行完剩余的C函数。
adjustresults(L, ci->nresults);
是因为此时一定停在了 luaD_precall
函数,而这后面就是这一句,因此可以写死,还有一句则是 luaD_poscall
。
1 | static void finishCcall (lua_State *L, int status) { |
lua_resume
以保护模式调用 resume
如果出现异常,则会调用 recover
去修复。可以看到 这里是去找 调用 pcall
的 CallInfo
。因为 pcall 确实会抛出异常,然后就会去找 pcall 在哪里,将其还未执行完的事情给完成(指的是 luaD_pcall 异常后应该做的事情)。
1 | static CallInfo *findpcall (lua_State *L) { |
yield
交出CPU资源,给其他协程机会,有了前面的基础,比较好理解,保存了当下次 resume
的时候,应该继续执行的C函数和上下文环境。
1 | LUA_API int lua_yieldk (lua_State *L, int nresults, lua_KContext ctx, |