0x1. 用法


在将协程的原理之前,先来回顾一下协程的用法。看一下lua官方的例子,这个例子可能有点绕,但是对于理解协程的前后调用关系还是很有帮助的。

  1. function foo (a)
  2. print("foo", a)
  3. return coroutine.yield(2*a)
  4. end
  5. co = coroutine.create(function (a,b)
  6. print("co-body", a, b)
  7. local r = foo(a+1)
  8. print("co-body", r)
  9. local r, s = coroutine.yield(a+b, a-b)
  10. print("co-body", r, s)
  11. return b, "end"
  12. end)
  13. print("main", coroutine.resume(co, 1, 10))
  14. print("main", coroutine.resume(co, "r"))
  15. print("main", coroutine.resume(co, "x", "y"))
  16. print("main", coroutine.resume(co, "x", "y"))
  17. --- When you run it, it produces the following output:
  18. co-body 1 10
  19. foo 2
  20. main true 4
  21. co-body r
  22. main true 11 -9
  23. co-body x y
  24. main true 10 end
  25. main false cannot resume dead coroutine
  26. ---

从中可以看出协程的主要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


image.png

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的时候退出虚拟机的执行即可返回到上一层的协程执行。

协程 - 图2