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