lua协程

协同程序与线程差不多,也就是一条执行序列,拥有自己独立的栈、局部变量和指令指针,同时又与其他协同程序共享全局变量和其他大部分东西。
一个具有多个协同程序的程序在任意时刻只能运行一个协同程序。

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))

与协同程序之间的对称性区别相比,协同程序与generatorPython所提供的)之间的区别很大。

实现

协程实现的两个关键点在于:

  • 协程状态的保存
  • 不同协程之间的数据通信机制

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); /* init stack */
setobj2n(L, gt(L1), gt(L)); /* share table of globals */
L1->hookmask = L->hookmask;
L1->basehookcount = L->basehookcount;
L1->hook = L->hook;
resethookcount(L1);
lua_assert(iswhite(obj2gco(L1)));
return L1;
}

接下来,我们来看看如何在不同协程之间通信,或者说Lua协程间数据的交换。 前面提到过resumeyield函数的参数就是用来做协程数据交换的,现在来看看里面的实现 。 奥秘就在函数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);//调用lua newthread创建lua State结构体
luaL_argcheck(L, lua_isfunction(L, 1) && !lua_iscfunction(L, 1), 1,//检查当前栈顶的元素是不是一个函数对象,因为需要一个函数作为协程开始运行时的主函数。 这个主函数必须是Lua函数, C函数将会报错
"Lua function expected");
lua_pushvalue(L, 1); /* move function to top *///将协程主函数压入当前 lua State 的栈中
lua_xmove(L, NL, 1); /* move function from L to NL *///调用 lua xmove 将该函数从当前的lua State移动到新创建的协程的 lua State栈中
return 1;
}

了解了Lua协程实现相关的数据结构,接下来看看最核心的两个操作resumeyield是如何实现的 。
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);//调用辅助函数auxresume进行实际的resume操作
if (r < 0) {//根据auxresume的返回值来做不同的处理。 当返回值小于0时,说明 resume操作出错,并且此时出错信息在栈顶,因此压入false以及出错消息
lua_pushboolean(L, 0);
lua_insert(L, -2);
return 2; /* return false + error message */
}
else {//否则, auxresume的返回值表示执行resume操作时返回的参数数量,这种情况下压入true以及这些返回参数
lua_pushboolean(L, 1);
lua_insert(L, -(r + 1));
return r + 1; /* return true + `resume' returns */
}
}

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; /* error flag */
}
lua_xmove(L, co, narg);//将参数通过lua_xmove函数传递到待启动的协程中
status = lua_resume(co, narg);//调用 lua_resume函数执行协程代码
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); /* move yielded values */// 当 lua_resume函数返回时,说明该协程已经执行完毕,通过lua_xmove函数将yield传入的参数传递回启动该协程的协程
return nres;
}
else {
lua_xmove(co, L, 1); /* move error message */
return -1; /* error flag */
}
}

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) { /* start coroutine? *///如果当前协程的状态是0 ,那么说明它是第一次执行 resume操作
lua_assert(ci == L->base_ci && firstArg > L->base);
if (luaD_precall(L, firstArg - 1, LUA_MULTRET) != PCRLUA)//调用 luaD_precall做函数调用前的准备工作
return;//如果luaD_precall函数的返回值不是PCRLUA ,说明是在C函数中进行resume操作的,此时并不需要后面的 luaV execute函数,就直接返回了
}
else { /* resuming from previous yield *///否则就从之前的 YIELD状态中继续执行
lua_assert(L->status == LUA_YIELD);
L->status = 0;//首先将协程的状态置为0
if (!f_isLua(ci)) { /* `common' yield? *///判断此时ci的类型
/* 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... *///如果不是Lua函数,说明之前是被中断的函数调用,此时调用luaD_poscall函数继续完成未完的函数操作
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;//否则只需要调整 base指针指向之前的ci的base指针即可
}
luaV_execute(L, cast_int(L->ci - L->base_ci));//以上的几种情况最终都会调用 luaV_execute 函数来进入 Lua虚拟机中执行 。 这里可以看到,由于使用了同样的结构lua State来表示Lua虚拟机和Lua协程,在表达Lua虚拟机的执行和协程的执行上,两者都是统一使用 luaV execute函数,方便了实现。
}

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; /* protect stack slots below */
L->status = LUA_YIELD;
lua_unlock(L);
return -1;
}

这个函数做的事情相比起来就简单多了,就是将协程执行状态至为YIELD,这样可以终止luaV_execute函数的循环。
捕获

从lua-5.1.1中分离出来的协程实现代码

lthread.c


Quien siembra vientos , recoge tempestades.