協程,簡單來說就是新創建一個協助程序(co = coroutine.create(func)),然后需要手動去啟動它(coroutine.resume(co)),在它最終退出之前,它有可能暫停多次返回階段性的結果(coroutine.yield(co)),每一次暫停之后都必須手動去恢復它(coroutine.resume(co))。
協程在lua源文件中對應lcorolib.c,數組co_funcs中定義了c暴露給lua的接口。從上面的描述看和c函數調用有點相似,只不過c函數只有一個出口,所以不可能返回多次。題外話,為什么c函數只有一個出口?我自己粗淺的理解是因為c函數的所有信息都放在棧上,而c語言沒有提供原生的保存/恢復棧空間的支持,所以沒有中途退出后還能生新進入這個概念。實際上,協程和系統級別的進程切換更像一點,都是保存堆棧,然后恢復。我想最大的不同就是協程知道接下來的控制權在哪里,而進程不知道。根本上它們想實現的功能就不一樣吧。
好了,那協程實現的要點就是堆棧的保存與恢復了。當然,這里的堆棧不是進程本身的堆棧,而是lua的soft stack。從代碼上來說吧:
82 static int luaB_cocreate (lua_State *L) { 83 lua_State *NL; 84 luaL_checktype(L, 1, LUA_TFUNCTION); 85 NL = lua_newthread(L); 86 lua_pushvalue(L, 1); /* move function to top */ 87 lua_xmove(L, NL, 1); /* move function from L to NL */ 88 return 1; 89 }
其中NL就是新創建的協程的棧,以后所有的保存/恢復都是針對這個棧。lua_State這個結構體里對協程實現最重要的是CallInfo *ci,CallInfo的定義如下:
66 /* 67 ** information about a call 68 */ 69 typedef struct CallInfo { 70 StkId func; /* function index in the stack */ 71 StkId top; /* top for this function */ 72 struct CallInfo *previous, *next; /* dynamic call link */ 73 short nresults; /* expected number of results from this function */ 74 lu_byte callstatus; 75 ptrdiff_t extra; 76 union { 77 struct { /* only for Lua functions */ 78 StkId base; /* base for this function */ 79 const Instruction *savedpc; 80 } l; 81 struct { /* only for C functions */ 82 int ctx; /* context info. in case of yields */ 83 lua_CFunction k; /* continuation in case of yields */ 84 ptrdiff_t old_errfunc; 85 lu_byte old_allowhook; 86 lu_byte status; 87 } c; 88 } u; 89 } CallInfo;
其中func指向當前調用的函數在棧上的位置,而savedpc就是保存的指令執行位置(先無視union里的c),根據這兩個值就能恢復函數的執行點。然而在yield的時候真正負責保存函數位置的是extra(保存func與棧頂的相對位置),在resume時func會根據extra來恢復,有沒有這個需要我是表示懷疑的,因為就算resume傳遞的參數導致棧realloc,使func失效,但在luaD_reallocstack內會調用correctstack將調用鏈上所有的func重新設置為正確的值,所以這里是不是多余的呢?
在lua 5.2中調用路徑包含c函數的時候也能夠進行yield,只不過不甚好看。由於c函數不能保存堆棧,所以lua的策略是直接放棄當前c函數的棧幀,而讓調用者本身提供一個continuation,當resume時調用上面被無視的uion里的c.k。沒用過,所以也不深入考究了。