1. 程式人生 > >lua協程實現

lua協程實現

協程是個很好的東西,它能做的事情與執行緒相似,區別在於:協程是使用者可控的,有API給使用者來暫停和繼續執行,而執行緒由作業系統核心控制;另外,協程也更加輕量級。這樣,在遇到某些可能阻塞的操作時,可以使用暫停協程讓出CPU;而當條件滿足時,可以繼續執行這個協程。目前在網路伺服器領域,使用Lua協程最好的範例就是ngx_lua了

來看看Lua協程內部是如何實現的。

本質上,每個Lua協程其實也是對應一個LuaState指標,所以其實它內部也是一個完整的Lua虛擬機器—有完整的Lua堆疊結構,函式呼叫棧等等等等,絕大部分之前對Lua虛擬機器的分析都可以直接套用到Lua協程中。於是,由Lua虛擬機器管理著這些隸屬於它的協程,當需要暫停當前執行協程的時候,就儲存它的執行環境,切換到別的協程繼續執行。很簡單的實現。

來看看相關的API。

  1. lua_newthread

建立一個Lua協程,最終會呼叫的API是luaE_newthread,Lua協程在Lua中也是一個獨立的Lua型別資料,它的型別是LUA_TTHREAD,建立完畢之後會照例初始化Lua的棧等結構,有一點需要注意的是,呼叫preinit_state初始化Lua協程的時候,傳入的global表指標是來自於Lua虛擬機器,換句話說,任何在Lua協程修改的全域性變數,也會影響到其他的Lua協程包括Lua虛擬機器本身。

  1. 載入一個Lua檔案並且執行

對於一般的Lua虛擬機器,大可以直接呼叫luaL_dofile即可,它其實是一個巨集:

#define luaL_dofile(L, fn) \
        (luaL_loadfile(L, fn) || lua_pcall(L, 0, LUA_MULTRET, 0))

展開來也就是當呼叫luaL_loadfile函式完成對該Lua檔案的解析,並且沒有錯誤時,呼叫lua_pcall函式執行這個Lua指令碼。

但是對於Lua協程而言,卻不能這麼做,需要呼叫luaL_loadfile然後再呼叫lua_resume函式。所以兩者的區別在於lua_pcall函式和lua_resume函式。來看看lua_resume函式的實現。這個函式做的幾件事情:首先檢視當前Lua協程的狀態對不對,然後修改計數器:

 L->baseCcalls = ++L->nCcalls;

其次呼叫status = luaD_rawrunprotected(L, resume, L->top – nargs);,可以看到這個保護Lua函式堆疊的呼叫luaD_rawrunprotected最終呼叫了函式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));
}

這個函式將執行Lua程式碼的流程劃分成了幾個階段,如果呼叫

luaD_precall(L, firstArg - 1, LUA_MULTRET) != PCRLUA

那麼說明這次呼叫返回的結果小於0,可以跟進luaD_precall函式看看什麼情況下會出現這樣的情況:

    n = (*curr_func(L)->c.f)(L);  /* do the actual call */
    lua_lock(L);
    if (n < 0)  /* yielding? */
      return PCRYIELD;
    else {
      luaD_poscall(L, L->top - n);
      return PCRC;
    }

繼續回到resume函式中,如果之前該Lua協程的狀態是YIELD,那麼說明之前被中斷了,則呼叫luaD_poscall完成這個函式的呼叫。
然後緊跟著呼叫luaV_execute繼續Lua虛擬機器的繼續執行。

可以看到,resume函式做的事情其實有那麼幾件:

  1. 如果呼叫C函式時被YIELD了,則直接返回
  2. 如果之前被YIELD了,則呼叫luaD_poscall完成這個函式的執行,接著呼叫luaV_execute繼續Lua虛擬機器的執行。

因此,這個函式對於函式執行中可能出現的YIELD,有充分的準備和判斷,因此它不像一般的pcall那樣,一股腦的往下執行,而是會在出現YIELD的時候儲存現場返回,在繼續執行的時候恢復現場。
3)同時,由於resume函式是由luaD_rawrunprotected進行保護呼叫的,即使執行出錯,也不會造成整個程式的退出。

這就是Lua協程中,比一般的Lua操作過程做的更多的地方。

最後給出一個Lua協程的例子:
co.lua

print("before")
test("123")
print("after resume")

co.c

 #include 
    #include "lua.h"
    #include "lualib.h"
    #include "lauxlib.h"

    static int panic(lua_State *state) {
      printf("PANIC: unprotected error in call to Lua API (%s)\n",
              lua_tostring(state, -1));
      return 0;
    }

    static int test(lua_State *state) {
      printf("in test\n");
      printf("yielding\n");
      return lua_yield(state, 0);
    }

    int main(int argc, char *argv[]) {
      char *name = NULL;
      name = "co.lua";
      lua_State*  L1 = NULL;
      L1 = lua_open();
      lua_atpanic(L1, panic);
      luaL_openlibs( L1 );

      lua_register(L1, "test", test);
      lua_State*  L = lua_newthread(L1);

      luaL_loadfile(L, name);
      lua_resume(L, 0);
      printf("sleeping\n");
      sleep(1);
      lua_resume(L, 0);
      printf("after resume test\n");

      return 0;
    }

你可以使用coroutine.create來建立協程,協程有三種狀態:掛起,執行,停止。建立後是掛起狀態,即不自動執行。status函式可以檢視當前狀態。協程真正強大的地方在於他可以通過yield函式將一段正在執行的程式碼掛起。

lua的resume-yield可以互相交換資料

co = coroutine.create(function (a, b)
     coroutine.yield(a+b, a-b)
end)
print(coroutine.resume(co, 3, 8))