標籤:
協程是個很好的東西,它能做的事情與線程相似,區別在於:協程是使用者可控的,有API給使用者來暫停和繼續執行,而線程由作業系統核心控制;另 外,協程也更加輕量級。這樣,在遇到某些可能阻塞的操作時,可以使用暫停協程讓出CPU;而當條件滿足時,可以繼續執行這個協程。目前在網路伺服器領域, 使用Lua協程最好的範例就是ngx_lua了,我自己的項目qnode也是藉助Lua協程的概念:每一個qnode中的微進程底層對應一個Lua協程, 這樣底層的非同步作業可以在使用者使用同步的方式寫出來。Coool。
來看看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虛擬機器本身。
2)載入一個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; }
[轉]-Lua協程的實現