请选择 进入手机版 | 继续访问电脑版

risc-v中文社区

 找回密码
 立即注册
查看: 1579|回复: 0

Lua源码分析 - 主流程篇 - 协程的实现(10)

[复制链接]

347

主题

564

帖子

2237

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
2237
发表于 2022-6-20 15:49:10 | 显示全部楼层 |阅读模式
目录

一、协程的实现 - Lua语言的协程使用

二、协程的实现 - 协程的设计思路coroutine

三、协程的实现 - 协程的创建luaB_cocreate

四、协程的实现 - 协程的启动和恢复luaB_coresume

五、协程的实现 - 协程的挂起luaB_yield

协程:协程不是进程或线程,其执行过程更类似于子例程,或者说不带返回值的函数调用。

Lua语言没有独立的线程,所以每次执行Lua脚本的时候,都是单线程执行。同一个执行过程中,Lua没有实现线程,但是实现了协程。

相比线程,线程相对资源独立,有自己的上下文,由系统切换调用
协程也相对独立,有自己的上下文,但是其切换由自己控制,由当前协程切换到其他协程由当前协程来控制。
一、协程的实现 - Lua语言的协程使用
-- 定义一个协程回调函数
function f ()
        print('--启动程序--');
    print('--中断程序--');
    coroutine.yield();
    print('--恢复程序--');
end
-- 为这个函数新建一个协程
co = coroutine.create(f);
print('--准备启动--');
coroutine.resume(co);
print('--准备恢复--');
coroutine.resume(co);

--------输出---------
--准备启动--
--启动程序--
--中断程序--
--准备恢复--
--恢复程序--
我们通过一个很简单的case,展示了Lua语言中,针对协程的运用。

本章节,我们重点讲解Lua协程的实现方式和原理。具体细节我们不会太展开。

从上面Lua语言中,我们可以看到,Lua调用了Lua的API函数库coroutine。协程API主要在lcorolib.c文件中。内置API如何加载到Lua语言中,并如何调用的,我们后续几章会重点去讲。

coroutine.create:创建一个协程,参数为回调函数(对应函数:luaB_cocreate)
coroutine.resume:启动或恢复一个程序(对应函数:luaB_coresume)
coroutine.yield:中断挂起一个协程。(对应函数:luaB_yield)
我们也会围绕这三个函数,来展开讲解。

二、协程的实现 - 协程的设计思路coroutine
这部分,我们主要讲解协程的设计思路,通过了解全局的设计思想,然后在结合源码阅读会简单很多。

Lua通过luaB_cocreate函数,去创建一个协程栈结构。这个协程栈结构是独立的数据栈结构,但是跟主线程栈共用了全局状态机global_State
当创建完新的协程栈,主线程上会将协程L和回调函数入栈(lua_pushvalue)。
一个协程回调函数里面,可能还会嵌套和包含其他的协程,所以,协程是支持嵌套的。
当我们调用luaB_coresume函数的时候,才会真正去执行协程的回调函数f。该函数有两种状态需要处理:正常状态 和 yield挂起中断状态。
正常状态处理:正常状态,最终会调用luaD_precall和luaV_execute,执行C闭包函数/内置API函数 或者 执行Lua字节码
中断状态处理:如果是中断状态,则在Lua的coroutine回调函数中,执行了luaB_yield方法。resume主要将中断的操作进行恢复
中断过程是通过抛出异常的方式完成的。当回调函数中调用luaB_yield中断处理的时候,会抛出一个LUA_YIELD的异常。resume函数是通过luaD_rawrunprotected异常保护方法去执行的,所以代码会跳到LUAI_TRY点。然后根据L->status状态判断是中断还是正常状态,执行不同的代码逻辑。


三、协程的实现 - 协程的创建luaB_cocreate
协程创建函数luaB_cocreate,主要做三件事情:

通过lua_newthread创建一个新的协程栈(lua_State)。协程栈独立管理内部的栈资源。
将CallInfo操作栈上的协程回调函数,移动到L->top数据栈顶部
拷贝回调函数到协程的数据栈上
/**
* 协程创建函数,会独立创建一个Lua栈结构
* Lua:newProductor = coroutine.create(productor)
* Lua创建一个协程的时候,入参为协程回调的函数名
*/
static int luaB_cocreate (lua_State *L) {
  lua_State *NL;
  luaL_checktype(L, 1, LUA_TFUNCTION);
  NL = lua_newthread(L); /* new一个新协程 */
  lua_pushvalue(L, 1);  /* 将CallInfo操作栈上的协程回调函数,移动到L->top数据栈顶部 move function to top */
  lua_xmove(L, NL, 1);  /* 拷贝回调函数到协程的数据栈上 move function from L to NL */
  return 1;
}

/**
* 创建一个新的线程栈
* LUA在main函数中,调用luaL_newstate()方法,创建了主线程(既:lua_State *L)
* 主要用于实现Lua的协程实现(Lua没有多线程实现)
*/
LUA_API lua_State *lua_newthread (lua_State *L) {
  global_State *g = G(L);
  lua_State *L1;
  lua_lock(L);
  luaC_checkGC(L);
  /* create new thread */
  L1 = &cast(LX *, luaM_newobject(L, LUA_TTHREAD, sizeof(LX)))->l;
  L1->marked = luaC_white(g);
  L1->tt = LUA_TTHREAD;
  /* link it on list 'allgc' */
  L1->next = g->allgc;
  g->allgc = obj2gco(L1);
  /* anchor it on L stack */
  setthvalue(L, L->top, L1); //栈顶上 设置一个新的L1对象
  api_incr_top(L);
  preinit_thread(L1, g);
  L1->hookmask = L->hookmask;
  L1->basehookcount = L->basehookcount;
  L1->hook = L->hook;
  resethookcount(L1);
  /* initialize L1 extra space */
  memcpy(lua_getextraspace(L1), lua_getextraspace(g->mainthread),
         LUA_EXTRASPACE);
  luai_userstatethread(L, L1);
  stack_init(L1, L);  /* init stack */
  lua_unlock(L);
  return L1;
}
四、协程的实现 - 协程的启动和恢复luaB_coresume
当创建完一个协程后,我们就需要启动该协程。luaB_coresume主要功能:启动 & 恢复 协程

该函数,首先getco方法中从主线程栈上获取协程栈结构(如果多层嵌套就是上一层的栈)。
然后调用auxresume方法。如果小于0表示出错,则栈顶是一个错误对象,此时在错误对象前面插入一个false;否则表示成功,栈顶是由协程函数或yield返回的参数,在这些参数之下插入一个true。
/**
* 启动/恢复协程程序
*/
static int luaB_coresume (lua_State *L) {
  lua_State *co = getco(L); //获取协程栈
  int r;
  r = auxresume(L, co, lua_gettop(L) - 1);
  if (r < 0) {
    lua_pushboolean(L, 0);
    lua_insert(L, -2);
    return 2;  /* return false + error message */
  }
  else {
    lua_pushboolean(L, 1);
    lua_insert(L, -(r + 1));
    return r + 1;  /* return true + 'resume' returns */
  }
}
看一下auxresume函数

L:表示原始线程栈
co:表示要启动的线程栈
如果返回值不是LUA_OK或LUA_YIELD,则表示出错:将co栈顶的错误对象转移到L栈顶
否则表示协程函数返回或中途有yield操作:将co栈中的返回值全部转移到L栈。
/**
* resume 核心操作方法
*/
static int auxresume (lua_State *L, lua_State *co, int narg) {
  int status;
  if (!lua_checkstack(co, narg)) {
    lua_pushliteral(L, "too many arguments to resume");
    return -1;  /* error flag */
  }
  if (lua_status(co) == LUA_OK && lua_gettop(co) == 0) {
    lua_pushliteral(L, "cannot resume dead coroutine");
    return -1;  /* error flag */
  }
  lua_xmove(L, co, narg); //将L上的栈数据拷贝到co上
  status = lua_resume(co, L, narg);
  if (status == LUA_OK || status == LUA_YIELD) {
    int nres = lua_gettop(co);
    if (!lua_checkstack(L, nres + 1)) {
      lua_pop(co, nres);  /* remove results anyway */
      lua_pushliteral(L, "too many results to resume");
      return -1;  /* error flag */
    }
    lua_xmove(co, L, nres);  /* move yielded values */
    return nres;
  }
  else {
    lua_xmove(co, L, 1);  /* move error message */
    return -1;  /* error flag */
  }
}
我们继续看下最核心lua_resume函数。

参数:L=当前启动栈,from=原始栈,nargs=参数个数
lua_resume没有参数用于指出期望的结果数量,它总是返回被调用函数的所有结果;
它没有用于指定错误处理函数的参数,发生错误时不会展开栈,这就可以在发生错误后检查栈中的情况;
如果正在运行的函数交出(yield)了控制权,lua_resume就会返回一个特殊的代码LUA_YIELD,并将线程置于一个可以被再次恢复执行的状态。
重要:最重要的是,lua_resume通过异常保护方法luaD_rawrunprotected,来调用执行的resume(中断挂起yield状态就是通过异常抛出来回到调用点的)
/**
* 启动一个协程程序,启动方式和lua_pcall相似,但是有3个区别
* 1. lua_resume没有参数用于指出期望的结果数量,它总是返回被调用函数的所有结果;
* 2. 它没有用于指定错误处理函数的参数,发生错误时不会展开栈,这就可以在发生错误后检查栈中的情况;
* 3. 如果正在运行的函数交出(yield)了控制权,lua_resume就会返回一个特殊的代码LUA_YIELD,并将线程置于一个可以被再次恢复执行的状态。
*
* L->nny = 0 设置允许挂起状态,协程栈上的操作,都会走luaD_call模式,而不会走luaD_callnoyield模式
*
* L:协程栈
* from:原始线程栈
* nargs:参数个数
*/
LUA_API int lua_resume (lua_State *L, lua_State *from, int nargs) {
  int status;
  unsigned short oldnny = L->nny;  /* save "number of non-yieldable" calls */
  lua_lock(L);
...省...
  status = luaD_rawrunprotected(L, resume, &nargs); //回调函数resume,入参L为协程栈
...省..
}

/**
* 异常保护方法
* 通过回调Pfunc f,并用setjmp和longjpm方式,实现代码的中断并回到setjmp处
*
*  #define LUAI_THROW(L,c)                longjmp((c)->b, 1)
*        #define LUAI_TRY(L,c,a)                if (setjmp((c)->b) == 0) { a }
*/
int luaD_rawrunprotected (lua_State *L, Pfunc f, void *ud) {
  unsigned short oldnCcalls = L->nCcalls;
  struct lua_longjmp lj;
  lj.status = LUA_OK;
  lj.previous = L->errorJmp;  /* chain new error handler */
  L->errorJmp = &lj;
  /* if (setjmp((c)->b) == 0) { a } */
  /* 当函数内部调用处理,遇到异常情况下,*/
  LUAI_TRY(L, &lj,
    (*f)(L, ud);
  );
  L->errorJmp = lj.previous;  /* restore old error handler */
  L->nCcalls = oldnCcalls;
  return lj.status;
}

我们看一下核心函数resume。

resume函数通过L->status去判断执行状态。
当L->status=LUA_OK,则正常启动一个函数调用流程
当L->status=LUA_YIELD,则恢复中断的协程调用,并将状态设置为LUA_OK恢复调用
中断恢复,我们需要调用luaD_poscall进行yield函数执行的时候的堆栈调整,然后调用unroll,执行恢复动作。
static void resume (lua_State *L, void *ud) {
  int n = *(cast(int*, ud));  /* number of arguments */
  StkId firstArg = L->top - n;  /* first argument */
  CallInfo *ci = L->ci;
  /* 如果L->status 不为中断状态(Lua中用法:coroutine.resume(co2)) */
  if (L->status == LUA_OK) {  /* starting a coroutine? */
    if (!luaD_precall(L, firstArg - 1, LUA_MULTRET))  /* Lua function? */
      luaV_execute(L);  /* call it */
  }
  else {  /* resuming from previous yield */

        /* 如果恢复中断挂起情况 */
    lua_assert(L->status == LUA_YIELD);
    L->status = LUA_OK;  /* 调整协程栈的状态 mark that it is running (again) */
    ci->func = restorestack(L, ci->extra); //取一下?
    if (isLua(ci))  /* yielded inside a hook? */
      luaV_execute(L);  /* 继续执行Lua代码 just continue running Lua code */
    else {  /* 通用的中断部分处理 'common' yield */
      if (ci->u.c.k != NULL) {  /* does it have a continuation function? */
        lua_unlock(L);
        n = (*ci->u.c.k)(L, LUA_YIELD, ci->u.c.ctx); /* call continuation */
        lua_lock(L);
        api_checknelems(L, n);
        firstArg = L->top - n;  /* yield results come from continuation */
      }
      //中断方法yield 为一个c语言lib方法,调整整体堆栈情况
      /* 中断处理逻辑:*/
      luaD_poscall(L, ci, firstArg, n);  /* 调整堆栈finish 'luaD_precall' */
    }
    unroll(L, NULL);  /* 执行先前中断的协程 run continuation */
  }
}
五、协程的实现 - 协程的挂起luaB_yield
协程挂起,主要执行luaB_yield函数。参数L,为当前的协程栈

为何参数L是当前协程栈呢?因为挂起函数,一般都是在协程回调函数内部使用。回调函数是被resume函数执行的,执行环境为当前协程栈环境。

/**
* 协程挂起函数
*
* L为当前协程函数的协程栈
* lua_gettop(L)  获取当前操作函数到栈顶的栈个数
*/
static int luaB_yield (lua_State *L) {
  return lua_yield(L, lua_gettop(L));
}
协程挂起是通过抛异常方式实现的。上面讲过resume函数是通过luaD_rawrunprotected方法进行保护型调用。当我们执行协程的回调函数,内部抛出LUA_YIELD状态的时候,就会回到setjmp的调用点。(前一章异常处理有详细解读)

/**
* 协程 - 方法中断操作
* luaD_throw(L, LUA_YIELD); //抛出一个LUA_YIELD
*/
LUA_API int lua_yieldk (lua_State *L, int nresults, lua_KContext ctx,
                        lua_KFunction k) {
  CallInfo *ci = L->ci; //获取操作栈
  luai_userstateyield(L, nresults);
  lua_lock(L);
  api_checknelems(L, nresults);
  if (L->nny > 0) { //如果L->nny>0的话,是不允许中断挂起
    if (L != G(L)->mainthread)
      luaG_runerror(L, "attempt to yield across a C-call boundary");
    else
      luaG_runerror(L, "attempt to yield from outside a coroutine");
  }
  L->status = LUA_YIELD; //中间状态
  ci->extra = savestack(L, ci->func);  /* 扩展字段上保存当前方法 save current 'func' */
  if (isLua(ci)) {  /* inside a hook? */
    api_check(L, k == NULL, "hooks cannot continue after yielding");
  }
  else {
    if ((ci->u.c.k = k) != NULL)  /* is there a continuation? */
      ci->u.c.ctx = ctx;  /* save context */
    ci->func = L->top - nresults - 1;  /* protect stack below results */
    luaD_throw(L, LUA_YIELD); //抛出一个LUA_YIELD
  }
  lua_assert(ci->callstatus & CIST_HOOKED);  /* must be inside a hook */
  lua_unlock(L);
  return 0;  /* return to 'luaD_hook' */
}

————————————————
版权声明:本文为CSDN博主「老码农zhuli」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/initphp/article/details/104296906

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则



Archiver|手机版|小黑屋|risc-v中文社区

GMT+8, 2024-4-17 04:01 , Processed in 0.020892 second(s), 17 queries .

risc-v中文社区论坛 官方网站

Copyright © 2018-2021, risc-v open source

快速回复 返回顶部 返回列表