0x1. 用法
在将协程的原理之前,先来回顾一下协程的用法。看一下lua官方的例子,这个例子可能有点绕,但是对于理解协程的前后调用关系还是很有帮助的。
function foo (a)
print("foo", a)
return coroutine.yield(2*a)
end
co = coroutine.create(function (a,b)
print("co-body", a, b)
local r = foo(a+1)
print("co-body", r)
local r, s = coroutine.yield(a+b, a-b)
print("co-body", r, s)
return b, "end"
end)
print("main", coroutine.resume(co, 1, 10))
print("main", coroutine.resume(co, "r"))
print("main", coroutine.resume(co, "x", "y"))
print("main", coroutine.resume(co, "x", "y"))
--- When you run it, it produces the following output:
co-body 1 10
foo 2
main true 4
co-body r
main true 11 -9
co-body x y
main true 10 end
main false cannot resume dead coroutine
---
从中可以看出协程的主要api就三个,下面我们会以这三个api为主线来讲解。
coroutine.create
:用于创建一个新的协程对象coroutine.resume
:启动协程的执行coroutine.yield
:协程让出控制权,返回调用resume
的协程中
0x2. 协程创建
static int luaB_cocreate (lua_State *L) {
lua_State *NL = lua_newthread(L);
luaL_argcheck(L, lua_isfunction(L, 1) && !lua_iscfunction(L, 1), 1,
"Lua function expected");
lua_pushvalue(L, 1); /* move function to top */
lua_xmove(L, NL, 1); /* move function from L to NL */
return 1;
}
这个比较简单,也就是创建一个新的lua_State
。我们都知道在lua中所有的数据基本都是保存在栈里面,因此这里还会把协程的函数放到新创建的lua_State
中。
0x3. rusume/yield
resume
static void resume (lua_State *L, void *ud) {
StkId firstArg = cast(StkId, ud);
CallInfo *ci = L->ci;
if (L->status == 0) { /* start coroutine? */
lua_assert(ci == L->base_ci && firstArg > L->base);
if (luaD_precall(L, firstArg - 1, LUA_MULTRET) != PCRLUA)
return;
}
else { /* resuming from previous yield */
lua_assert(L->status == LUA_YIELD);
L->status = 0;
if (!f_isLua(ci)) { /* `common' yield? */
/* finish interrupted execution of `OP_CALL' */
lua_assert(GET_OPCODE(*((ci-1)->savedpc - 1)) == OP_CALL ||
GET_OPCODE(*((ci-1)->savedpc - 1)) == OP_TAILCALL);
if (luaD_poscall(L, firstArg)) /* complete it... */
L->top = L->ci->top; /* and correct top if not multiple results */
}
else /* yielded inside a hook: just continue its execution */
L->base = L->ci->base;
}
luaV_execute(L, cast_int(L->ci - L->base_ci));
}
resume
的逻辑简单来说就是,恢复lua_State
的函数栈,然后调用虚拟机执行函数luaV_execute
进入新协程的执行。
yield
LUA_API int lua_yield (lua_State *L, int nresults) {
luai_userstateyield(L, nresults);
lua_lock(L);
if (L->nCcalls > L->baseCcalls)
luaG_runerror(L, "attempt to yield across metamethod/C-call boundary");
L->base = L->top - nresults; /* protect stack slots below */
L->status = LUA_YIELD;
lua_unlock(L);
return -1;
}
这个lua_yield
咋一看好像没有做什么切换相关的东西。其实这里的重点在于返回值,返回值< 0
的会退出虚拟机的执行。这里可能会用个疑惑,就是调用coroutine.yield
的时候并没有传递要返回的lua_State
,那么这个是如何找到的。这个从下面函数调用关系就可以看得出来,其实只要虚拟机执行到yield的时候退出虚拟机的执行即可返回到上一层的协程执行。