lua協程實現
協程是個很好的東西,它能做的事情與執行緒相似,區別在於:協程是使用者可控的,有API給使用者來暫停和繼續執行,而執行緒由作業系統核心控制;另外,協程也更加輕量級。這樣,在遇到某些可能阻塞的操作時,可以使用暫停協程讓出CPU;而當條件滿足時,可以繼續執行這個協程。目前在網路伺服器領域,使用Lua協程最好的範例就是ngx_lua了
來看看Lua協程內部是如何實現的。
本質上,每個Lua協程其實也是對應一個LuaState指標,所以其實它內部也是一個完整的Lua虛擬機器—有完整的Lua堆疊結構,函式呼叫棧等等等等,絕大部分之前對Lua虛擬機器的分析都可以直接套用到Lua協程中。於是,由Lua虛擬機器管理著這些隸屬於它的協程,當需要暫停當前執行協程的時候,就儲存它的執行環境,切換到別的協程繼續執行。很簡單的實現。
來看看相關的API。
- lua_newthread
建立一個Lua協程,最終會呼叫的API是luaE_newthread,Lua協程在Lua中也是一個獨立的Lua型別資料,它的型別是LUA_TTHREAD,建立完畢之後會照例初始化Lua的棧等結構,有一點需要注意的是,呼叫preinit_state初始化Lua協程的時候,傳入的global表指標是來自於Lua虛擬機器,換句話說,任何在Lua協程修改的全域性變數,也會影響到其他的Lua協程包括Lua虛擬機器本身。
- 載入一個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函式做的事情其實有那麼幾件:
- 如果呼叫C函式時被YIELD了,則直接返回
- 如果之前被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))