协同程序与线程差不多,也就是一条执行序列,拥有自己独立的栈、局部变量和指令指针,同时又与其他协同程序共享全局变量和其他大部分东西。
一个具有多个协同程序的程序在任意时刻只能运行一个协同程序。
lua
将所有关于协同程序的函数放置在一个名为“coroutine”的table
中。
一个协同程序可以处于4种不同的状态:挂起、运行、死亡和正常。
程序初创建:挂起
程序运行:运行
程序结束:死亡
程序被其实程序唤醒:正常
co_create
传一个函数参数,用来创建协程。返回一个“thread”对象:
1 2 3 4
| co = coroutine.create(function (a,b) coroutine.yield(a+b,a-b) end) print(coroutine.resume(co,20,10))
|
与协同程序之间的对称性区别相比,协同程序与generator
(Python
所提供的)之间的区别很大。
实现
协程实现的两个关键点在于:
在Lua
代码中,使用的是lua_State
结构体来表示协程,这与Lua
虚拟机用的是同一个数据结构 。 这一点可以从创建协程的函数lua_newthread
中看出来,唯一有区别的是, Lua
协程的类型是LUA_TTHREAD
。 换言之,在Lua
源码的处理中, Lua
协程与Lua
虚拟机的表现形式并没有太大差异,也许这样做是为了实现方便。 前面提到过,一个协程有自己私有的环境,不会因为协程的切换而发生改变 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| LUA_API lua_State *lua_newthread (lua_State *L) { lua_State *L1; lua_lock(L); luaC_checkGC(L); L1 = luaE_newthread(L); setthvalue(L, L->top, L1); api_incr_top(L); lua_unlock(L); luai_userstatethread(L, L1); return L1; }
lua_State *luaE_newthread (lua_State *L) { lua_State *L1 = tostate(luaM_malloc(L, state_size(lua_State))); luaC_link(L, obj2gco(L1), LUA_TTHREAD); preinit_state(L1, G(L)); stack_init(L1, L); setobj2n(L, gt(L1), gt(L)); L1->hookmask = L->hookmask; L1->basehookcount = L->basehookcount; L1->hook = L->hook; resethookcount(L1); lua_assert(iswhite(obj2gco(L1))); return L1; }
|
接下来,我们来看看如何在不同协程之间通信,或者说Lua
协程间数据的交换。 前面提到过resume
和yield
函数的参数就是用来做协程数据交换的,现在来看看里面的实现 。 奥秘就在函数lua_xmove
中。
1 2 3 4 5 6 7 8 9 10 11 12 13
| LUA_API void lua_xmove (lua_State *from, lua_State *to, int n) { int i; if (from == to) return; lua_lock(to); api_checknelems(from, n); api_check(from, G(from) == G(to)); api_check(from, to->ci->top - to->top >= n); from->top -= n; for (i = 0; i < n; i++) { setobj2s(to, to->top++, from->top + i); } lua_unlock(to); }
|
这段代码做的事情就是,从from
协程中移动n
个数据到to
协程中 。 当然在移动之前,数据要在from
协程的栈顶上准备好。
创建协程在函数luaB _cocreate
中进行.
1 2 3 4 5 6 7 8
| 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); lua_xmove(L, NL, 1); return 1; }
|
了解了Lua
协程实现相关的数据结构,接下来看看最核心的两个操作resume
和yield
是如何实现的 。
resume
操作在函数luaB_coresume
中实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| static int luaB_coresume (lua_State *L) { lua_State *co = lua_tothread(L, 1); int r; luaL_argcheck(L, co, 1, "coroutine expected"); r = auxresume(L, co, lua_gettop(L) - 1); if (r < 0) { lua_pushboolean(L, 0); lua_insert(L, -2); return 2; } else { lua_pushboolean(L, 1); lua_insert(L, -(r + 1)); return r + 1; } }
|
auxresume
函数的实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| static int auxresume (lua_State *L, lua_State *co, int narg) { int status; if (!lua_checkstack(co, narg)) luaL_error(L, "too many arguments to resume"); if (lua_status(co) == 0 && lua_gettop(co) == 0) { lua_pushliteral(L, "cannot resume dead coroutine"); return -1; } lua_xmove(L, co, narg); status = lua_resume(co, narg); if (status == 0 || status == LUA_YIELD) { int nres = lua_gettop(co); if (!lua_checkstack(L, nres)) luaL_error(L, "too many results to resume"); lua_xmove(co, L, nres); return nres; } else { lua_xmove(co, L, 1); return -1; } }
|
auxresume
函数会调用lua resume
函数,在lua resume
函数中进行一些检查,比如当前的状态是否合理,调用层次是否过多,最终使用luaD_rawrunprotected
函数来保护调用resume
函数 。
resume
函数的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| static void resume (lua_State *L, void *ud) { StkId firstArg = cast(StkId, ud); CallInfo *ci = L->ci; if (L->status == 0) { lua_assert(ci == L->base_ci && firstArg > L->base); if (luaD_precall(L, firstArg - 1, LUA_MULTRET) != PCRLUA) return; } else { lua_assert(L->status == LUA_YIELD); L->status = 0; if (!f_isLua(ci)) { lua_assert(GET_OPCODE(*((ci-1)->savedpc - 1)) == OP_CALL || GET_OPCODE(*((ci-1)->savedpc - 1)) == OP_TAILCALL); if (luaD_poscall(L, firstArg)) L->top = L->ci->top; } else L->base = L->ci->base; } luaV_execute(L, cast_int(L->ci - L->base_ci)); }
|
yield
操作在函数lua_yield
中进行:
1 2 3 4 5 6 7 8 9 10
| LUA_API int lua_yield (lua_State *L, int nresults) { luai_userstateyield(L, nresults); lua_lock(L); if (L->nCcalls > 0) luaG_runerror(L, "attempt to yield across metamethod/C-call boundary"); L->base = L->top - nresults; L->status = LUA_YIELD; lua_unlock(L); return -1; }
|
这个函数做的事情相比起来就简单多了,就是将协程执行状态至为YIELD
,这样可以终止luaV_execute
函数的循环。
从lua-5.1.1中分离出来的协程实现代码
lthread.c
Quien siembra vientos , recoge tempestades.