C++與Lua協(xié)程交互的示例詳解
零、前言
Lua 語言不支持真正的多線程,即不支持共享內(nèi)存的搶占式線程。
這樣的模式能減少一些多線程的問題。多線程的問題源于線程搶占和共享內(nèi)存,而如果非搶占式線程或者不使用共享內(nèi)存則能避免多線程問題,Lua 同時支持這兩種方案。從之前分享的《Lua 協(xié)程》文章中知道:
- Lua 語言的線程是協(xié)作式的,即協(xié)程,可以避免因不可預(yù)知的線程切換帶來的問題。
- Lua 狀態(tài)間內(nèi)存不共享,所以各個狀態(tài)相互獨(dú)立運(yùn)行,可以并行操作。
一、多線程
從 C-API 的角度,可以把線程當(dāng)作一個棧,每個棧保存著一個線程中掛起的函數(shù)調(diào)用信息,以及每個函數(shù)調(diào)用的參數(shù)和局部變量。也就是說,一個棧包括了一個線程得以繼續(xù)運(yùn)行所需的所有信息。 因此,要達(dá)到多線程就需要多個棧。
每當(dāng)創(chuàng)建一個 lua_State 時,Lua 就會自動用這個 lua_State 創(chuàng)建一個主線程。這個主線程永遠(yuǎn)不會被垃圾回收,只有當(dāng)調(diào)用 lua_close 關(guān)閉時才會釋放。
可以通過 C-API lua_newthread
在已有的 lua_State 中創(chuàng)建新的線程。
二、lua_newthread
lua_State *(lua_newthread) (lua_State *L);
描述:
這個函數(shù)會將新線程 thread 類型的值壓入棧中,并返回一個表示該線程的 lua_State 類型的指針。
新線程和舊線程的異同點(diǎn):
可以使用以下代碼創(chuàng)建了一個新的線程,并且得到一個新的 lua_State
指針類型的 L1 值。
lua_State *L1 = lua_newthread(L);
- 新線程 L1 與主線程 L 共享全局變量和注冊表,但是它們具有獨(dú)立的??臻g。新線程 L1 從空棧開始,主線程 L 則在其棧頂會引用 L1 這個新線程。
- 除了主線程 L 需要使用
lua_close
進(jìn)行關(guān)閉,新線程 L1 和其他的 Lua 對象一樣都是垃圾回收的對象。所以永遠(yuǎn)不要使用未被正確錨定在 lua_State 中的線程(主線程是內(nèi)部錨定,因此不用擔(dān)心會被回收),因為所有對 Lua API 的調(diào)用都有可能回收未錨定的線程,即使是正在使用這個線程進(jìn)行函數(shù)調(diào)用。 要避免這種情況,應(yīng)該在 “已錨定的線程的棧”、“注冊表”、“Lua 變量” 中保留對使用中線程的引用,防止被垃圾回收機(jī)制回收。 - 擁有的新線程 L1 可以像主線程 L 一樣使用,進(jìn)行壓棧調(diào)用函數(shù)等操作。
舉個例子:
- 創(chuàng)建一個新的線程。
- 分別打印各自棧的內(nèi)容。
lua_State *L = luaL_newstate(); luaL_openlibs(L); // 用 L 創(chuàng)建一個新的線程 // L 和 L1 各自有一個棧 // L 的棧頂是 L1 的 thread // L1 的棧是空 lua_State *L1 = lua_newthread(L); printf("主線程 L 棧深度:%d\n", lua_gettop(L)); printf("主線程 L 棧內(nèi)容:------------\n"); stackDump(L); printf("新線程 L1 棧深度:%d\n", lua_gettop(L1)); printf("新線程 L1 棧內(nèi)容:------------\n"); stackDump(L1); lua_close(L);
輸出如下:
主線程 L 棧深度:1
------------ 主線程 L 棧內(nèi)容:------------
棧頂
^ typename: thread, value: thread
棧底新線程 L1 棧深度:0
------------ 新線程 L1 棧內(nèi)容:------------
棧頂
棧底
三、新線程的作用
如果創(chuàng)建一個新的線程,只是用來運(yùn)行簡單的函數(shù),這種場景其實只需要在主線程中執(zhí)行即可。
創(chuàng)建新線程的主要目的是運(yùn)行協(xié)程。 可以在新線程中掛起協(xié)程,然后繼續(xù)運(yùn)行其他線程的 Lua 代碼,在需要的節(jié)點(diǎn)恢復(fù)協(xié)程,使新線程繼續(xù)執(zhí)行掛起點(diǎn)之后的邏輯代碼,而運(yùn)行協(xié)程需要用到 lua_resume
C-API 。
四、lua_resume
LUA_API int (lua_resume) (lua_State *L, lua_State *from, int narg, int *nres);
描述:
使用 lua_resume
和使用 lua_pcall
調(diào)用函數(shù)很相似,壓入?yún)f(xié)程的參數(shù),然后將待調(diào)用函數(shù)(協(xié)程體)壓入棧,并以參數(shù)的數(shù)量作為參數(shù) narg
調(diào)用 lua_resume
。
lua_resume
和 lua_pcall
的不同點(diǎn):
lua_resume
中沒有表示期望結(jié)果數(shù)量的參數(shù),總是返回被調(diào)用函數(shù)的所有結(jié)果。- 沒有錯誤處理函數(shù)的參數(shù),發(fā)生錯誤時不會進(jìn)行棧展開,可以在錯誤發(fā)生后檢查棧的情況。
- 如果正在運(yùn)行的函數(shù)被掛起,
lua_resume
就會返回代碼LUA_YIELD
,并將線程置于一個后續(xù)可以恢復(fù)執(zhí)行的狀態(tài)中。
參數(shù):
- 參數(shù) L:Lua State 的指針。
- 參數(shù) from:正在執(zhí)行調(diào)用的線程,或為 NULL 。
- 參數(shù) narg:入?yún)?shù)數(shù)量。
- 參數(shù) nres:協(xié)程體返回的值數(shù)量。
返回值:
返回當(dāng)前協(xié)程體處于哪個狀態(tài):
- 如果被掛起,則返回
LUA_YIELD
。 - 如果已經(jīng)執(zhí)行完成,則返回
LUA_OK
。 - 如果 Lua 腳本中拋出異常,則會返回
LUA_ERRxxx
類型的錯誤。
值傳遞:
lua_Stack 的棧是相互獨(dú)立,所以此時如果需要將協(xié)程產(chǎn)出的數(shù)據(jù)傳遞到另一個 lua_State 中。
- 可以將需要的數(shù)據(jù)通過 C-API 獲取至 C 中,處理之后,再壓入另一個 lua_State 的棧中。
- 可以使用
lua_xmove
將數(shù)據(jù)傳遞至另一個 lua_State 中。
Lua 調(diào)用 C++ 時,C++ 函數(shù)掛起協(xié)程體:
在執(zhí)行協(xié)程體的 Lua 腳本時,Lua 同樣可以調(diào)用 C++ 的函數(shù)。C++ 函數(shù)體內(nèi)也可以進(jìn)行協(xié)程掛起,通過 lua_yieldk
便可以進(jìn)行達(dá)到協(xié)程掛起。
五、lua_yieldk
LUA_API int (lua_yieldk) (lua_State *L, int nresults, lua_KContext ctx, lua_KFunction k);
描述:
將正在運(yùn)行的協(xié)程體立即掛起。
當(dāng)協(xié)程恢復(fù)運(yùn)行時,控制權(quán)會直接交給延續(xù)函數(shù) k 。當(dāng)協(xié)程交出控制后,調(diào)用 lua_yield
的函數(shù)后續(xù)語句就不會被執(zhí)行了。
參數(shù):
- 參數(shù) L:Lua State 的指針。
- 參數(shù) nresults:將要返回給對應(yīng)的
lua_resume
的棧中值的個數(shù)。 - 參數(shù) ctx:傳遞給延續(xù)的上下文信息。
- 參數(shù) k:延續(xù)函數(shù)。
值得注意:
如果 C++ 函數(shù)交出控制權(quán)之后,無需做后續(xù)處理,即掛起再恢復(fù)后,無需執(zhí)行后續(xù)操作,可以不設(shè)置參數(shù) k (設(shè)置為 NULL)。具體代碼如下所示:
#define lua_yield(L,n) lua_yieldk(L, (n), 0, NULL)
六、C++ 與 Lua 協(xié)程交互的例子
- 創(chuàng)建一個新線程 lua_State ,在這個新線程中執(zhí)行協(xié)程體。
- 協(xié)程體內(nèi),首先會在 Lua 腳本中掛起,并返回數(shù)據(jù)到宿主中。
- 恢復(fù)協(xié)程體后,Lua 從掛起點(diǎn)繼續(xù)運(yùn)行,調(diào)用 C++ 函數(shù),C++ 函數(shù)內(nèi)掛起協(xié)程體,并返回數(shù)據(jù)到宿主。
- 再次恢復(fù)協(xié)程后,運(yùn)行 C++ 函數(shù)掛起點(diǎn)的延續(xù)函數(shù),運(yùn)行完延續(xù)函數(shù),回至 Lua 調(diào)用 C++ 調(diào)用點(diǎn)繼續(xù)執(zhí)行至結(jié)束,返回數(shù)據(jù)到宿主。
第一步,編寫 C++ 函數(shù),暴露給 Lua 調(diào)用。
// 延續(xù)函數(shù) int cfooK(lua_State *L, int status, lua_KContext ctx) { printf("恢復(fù)協(xié)程體后,調(diào)用 C++ 延續(xù)函數(shù)\n"); return 1; } // 暴露給 lua 調(diào)用的 C++ 函數(shù) int primCFunction(lua_State *L) { lua_pushstring(L, "調(diào)用 C++ 函數(shù),會進(jìn)行掛起"); // 設(shè)置了 cfooK 作為延續(xù)函數(shù),恢復(fù)協(xié)程之后,會進(jìn)入 cfook 這一延續(xù)函數(shù) lua_yieldk(L, 1, 0, &cfooK); // 這兩種的使用結(jié)果是一樣的,都不舍之延續(xù)函數(shù) // lua_yieldk(L, 1, 0, nullptr); // lua_yield(L, 1); // 不會被執(zhí)行,恢復(fù)之后會運(yùn)行延續(xù)函數(shù),不會執(zhí)行后續(xù)的語句 printf("掛起之后的輸(不會被輸出)"); return 1; }
第二步,編寫 Lua 腳本文件。
function foo(x) -- 協(xié)程掛起,并返回兩個數(shù)據(jù) coroutine.yield(10, x) end function foo1(x) foo(x + 1) primCFunction() return 3 end
第三步,最后編寫運(yùn)行代碼。
void useThread() { lua_State *L = luaL_newstate(); luaL_openlibs(L); lua_State *L1 = lua_newthread(L); lua_pushcfunction(L1, primCFunction); lua_setglobal(L1, "primCFunction"); std::string fname = PROJECT_PATH + "/12、線程和狀態(tài)/thread/coroutine.lua"; if (luaL_loadfile(L1, fname.c_str()) || lua_pcall(L1, 0, 0, 0)) { printf("can't run config. file: %s", lua_tostring(L1, -1)); } printf("LUA_YIELD=%d\n", LUA_YIELD); printf("LUA_OK=%d\n", LUA_OK); // 返回值個數(shù) int result = 0; // 獲取 L1 中的 lua 文件的函數(shù) foo1 lua_getglobal(L1, "foo1"); // 壓入 integer lua_pushinteger(L1, 20); printf("第一次調(diào)用,Lua 腳本中掛起:\n"); auto state = lua_resume(L1, L, 1, &result); printf("協(xié)程狀態(tài): %s\n", getResumeState(state).c_str()); printf("L1 棧深度: %d\n", lua_gettop(L1)); printf("返回值個數(shù): %d\n", result); printf("------------ L1 棧內(nèi)容:------------\n"); stackDump(L1); printf("第二次調(diào)用,Lua 調(diào)用 C++ ,C++ 中掛起:\n"); state = lua_resume(L1, L, 0, &result); printf("協(xié)程狀態(tài): %s\n", getResumeState(state).c_str()); printf("L1 棧深度: %d\n", lua_gettop(L1)); printf("返回值個數(shù): %d\n", result); printf("------------ L1 棧內(nèi)容:------------\n"); stackDump(L1); printf("第三次調(diào)用,運(yùn)行 C++ 延續(xù)函數(shù),協(xié)程體結(jié)束::\n"); state = lua_resume(L1, L, 0, &result); printf("協(xié)程狀態(tài): %s\n", getResumeState(state).c_str()); printf("L1 棧深度: %d\n", lua_gettop(L1)); printf("返回值個數(shù): %d\n", result); printf("------------ L1 棧內(nèi)容:------------\n"); stackDump(L1); lua_close(L); }
最后運(yùn)行的結(jié)果如下,這里就不再一個個步驟拆解了,代碼有詳細(xì)的注釋,結(jié)合運(yùn)行的結(jié)果便可分析出流轉(zhuǎn)的過程了。
LUA_YIELD=1 LUA_OK=0 第一次調(diào)用,Lua 腳本中掛起: 協(xié)程狀態(tài): LUA_YIELD L1 棧深度: 2 返回值個數(shù): 2 ------------ L1 棧內(nèi)容:------------ 棧頂 ^ typename: number, value(integer): 21 ^ typename: number, value(integer): 10 棧底 第二次調(diào)用,Lua 調(diào)用 C++ ,C++ 中掛起: 協(xié)程狀態(tài): LUA_YIELD L1 棧深度: 1 返回值個數(shù): 1 ------------ L1 棧內(nèi)容:------------ 棧頂 ^ typename: string, value: '調(diào)用 C++ 函數(shù),會進(jìn)行掛起' 棧底 第三次調(diào)用,運(yùn)行 C++ 延續(xù)函數(shù),協(xié)程體結(jié)束:: 協(xié)程狀態(tài): LUA_OK L1 棧深度: 1 返回值個數(shù): 1 ------------ L1 棧內(nèi)容:------------ 棧頂 ^ typename: number, value(integer): 3 棧底
七、lua_xmove
LUA_API void (lua_xmove) (lua_State *from, lua_State *to, int n);
描述:
交換同一個狀態(tài)機(jī)下不同線程中的值,會從 from 的棧上彈出 n 個值, 然后把它們壓入 to 的棧上。
舉個例子:
- 創(chuàng)建一個 lua_State "L",然后從這一 lua_State 創(chuàng)建一個新的線程 lua_State "L1"。
- 通過 L1 執(zhí)行 Lua 腳本,得到兩個數(shù)據(jù)。
- 將 L1 的數(shù)據(jù)移動到 L 中,打印結(jié)果。
第一步,編寫 lua 腳本。
function foo(x) return "江澎涌", x * x end
第二步,編寫主函數(shù)。
void copyStackElement() { lua_State *L = luaL_newstate(); luaL_openlibs(L); lua_State *L1 = lua_newthread(L); // 新線程中執(zhí)行 Lua 腳本 std::string fname = PROJECT_PATH + "/12、線程和狀態(tài)/thread/copy_stack.lua"; if (luaL_loadfile(L1, fname.c_str()) || lua_pcall(L1, 0, 0, 0)) { printf("can't run config. file: %s", lua_tostring(L1, -1)); } // 獲取 L1 中的 lua 文件的函數(shù) foo lua_getglobal(L1, "foo"); // 壓入 integer lua_pushinteger(L1, 5); // 調(diào)用函數(shù) foo(5) -> "江澎涌", 25 lua_call(L1, 1, 2); printf("------------ 主線程 L 棧內(nèi)容(xmove 前):--------------\n"); stackDump(L); printf("------------ 新線程 L1 棧內(nèi)容(xmove 前):--------------\n"); stackDump(L1); // 從 L1 中拷貝 1 個元素到 L 中(會將 L1 元素彈出) lua_xmove(L1, L, 2); printf("------------ 主線程 L 棧內(nèi)容(xmove 后):--------------\n"); stackDump(L); printf("------------ 新線程 L1 棧內(nèi)容(xmove 后):--------------\n"); stackDump(L1); }
八、寫在最后
Lua 項目地址:Github傳送門
以上就是C++與Lua協(xié)程交互的示例詳解的詳細(xì)內(nèi)容,更多關(guān)于C++與Lua協(xié)程交互的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
VC基于ADO技術(shù)訪問數(shù)據(jù)庫的方法
這篇文章主要介紹了VC基于ADO技術(shù)訪問數(shù)據(jù)庫的方法,較為詳細(xì)的分析了VC使用ADO操作數(shù)據(jù)庫的相關(guān)實現(xiàn)技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-10-10關(guān)于在MFC中將窗口最小化到托盤實現(xiàn)原理及操作步驟
最小化的原理:首先要將窗口隱藏,然后在右下角繪制圖標(biāo);恢復(fù)的原理:將窗口顯示,再將托盤中的圖片刪除,接下來介紹實現(xiàn)方法,感興趣的朋友可以了解下啊,希望本文對你有所幫助2013-01-01C++實現(xiàn)KFC點(diǎn)餐系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了C++實現(xiàn)KFC點(diǎn)餐系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2020-07-07C語言實現(xiàn)通用數(shù)據(jù)結(jié)構(gòu)之通用鏈表
這篇文章主要為大家詳細(xì)介紹了c語言實現(xiàn)通用數(shù)據(jù)結(jié)構(gòu)之通用鏈表,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-11-11