C++与Lua协程交互的示例详解
零、前言
Lua 语言不支持真正的多线程,即不支持共享内存的抢占式线程。
这样的模式能减少一些多线程的问题。多线程的问题源于线程抢占和共享内存,而如果非抢占式线程或者不使用共享内存则能避免多线程问题,Lua 同时支持这两种方案。从之前分享的《Lua 协程》文章中知道:
- Lua 语言的线程是协作式的,即协程,可以避免因不可预知的线程切换带来的问题。
- Lua 状态间内存不共享,所以各个状态相互独立运行,可以并行操作。
一、多线程
从 C-API 的角度,可以把线程当作一个栈,每个栈保存着一个线程中挂起的函数调用信息,以及每个函数调用的参数和局部变量。也就是说,一个栈包括了一个线程得以继续运行所需的所有信息。 因此,要达到多线程就需要多个栈。
每当创建一个 lua_State 时,Lua 就会自动用这个 lua_State 创建一个主线程。这个主线程永远不会被垃圾回收,只有当调用 lua_close 关闭时才会释放。
可以通过 C-API lua_newthread
在已有的 lua_State 中创建新的线程。
二、lua_newthread
lua_State *(lua_newthread) (lua_State *L);
描述:
这个函数会将新线程 thread 类型的值压入栈中,并返回一个表示该线程的 lua_State 类型的指针。
新线程和旧线程的异同点:
可以使用以下代码创建了一个新的线程,并且得到一个新的 lua_State
指针类型的 L1 值。
lua_State *L1 = lua_newthread(L);
- 新线程 L1 与主线程 L 共享全局变量和注册表,但是它们具有独立的栈空间。新线程 L1 从空栈开始,主线程 L 则在其栈顶会引用 L1 这个新线程。
- 除了主线程 L 需要使用
lua_close
进行关闭,新线程 L1 和其他的 Lua 对象一样都是垃圾回收的对象。所以永远不要使用未被正确锚定在 lua_State 中的线程(主线程是内部锚定,因此不用担心会被回收),因为所有对 Lua API 的调用都有可能回收未锚定的线程,即使是正在使用这个线程进行函数调用。 要避免这种情况,应该在 “已锚定的线程的栈”、“注册表”、“Lua 变量” 中保留对使用中线程的引用,防止被垃圾回收机制回收。 - 拥有的新线程 L1 可以像主线程 L 一样使用,进行压栈调用函数等操作。
举个例子:
- 创建一个新的线程。
- 分别打印各自栈的内容。
lua_State *L = luaL_newstate(); luaL_openlibs(L); // 用 L 创建一个新的线程 // L 和 L1 各自有一个栈 // L 的栈顶是 L1 的 thread // L1 的栈是空 lua_State *L1 = lua_newthread(L); printf("主线程 L 栈深度:%d\n", lua_gettop(L)); printf("主线程 L 栈内容:------------\n"); stackDump(L); printf("新线程 L1 栈深度:%d\n", lua_gettop(L1)); printf("新线程 L1 栈内容:------------\n"); stackDump(L1); lua_close(L);
输出如下:
主线程 L 栈深度:1
------------ 主线程 L 栈内容:------------
栈顶
^ typename: thread, value: thread
栈底新线程 L1 栈深度:0
------------ 新线程 L1 栈内容:------------
栈顶
栈底
三、新线程的作用
如果创建一个新的线程,只是用来运行简单的函数,这种场景其实只需要在主线程中执行即可。
创建新线程的主要目的是运行协程。 可以在新线程中挂起协程,然后继续运行其他线程的 Lua 代码,在需要的节点恢复协程,使新线程继续执行挂起点之后的逻辑代码,而运行协程需要用到 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
调用函数很相似,压入协程的参数,然后将待调用函数(协程体)压入栈,并以参数的数量作为参数 narg
调用 lua_resume
。
lua_resume
和 lua_pcall
的不同点:
lua_resume
中没有表示期望结果数量的参数,总是返回被调用函数的所有结果。- 没有错误处理函数的参数,发生错误时不会进行栈展开,可以在错误发生后检查栈的情况。
- 如果正在运行的函数被挂起,
lua_resume
就会返回代码LUA_YIELD
,并将线程置于一个后续可以恢复执行的状态中。
参数:
- 参数 L:Lua State 的指针。
- 参数 from:正在执行调用的线程,或为 NULL 。
- 参数 narg:入参参数数量。
- 参数 nres:协程体返回的值数量。
返回值:
返回当前协程体处于哪个状态:
- 如果被挂起,则返回
LUA_YIELD
。 - 如果已经执行完成,则返回
LUA_OK
。 - 如果 Lua 脚本中抛出异常,则会返回
LUA_ERRxxx
类型的错误。
值传递:
lua_Stack 的栈是相互独立,所以此时如果需要将协程产出的数据传递到另一个 lua_State 中。
- 可以将需要的数据通过 C-API 获取至 C 中,处理之后,再压入另一个 lua_State 的栈中。
- 可以使用
lua_xmove
将数据传递至另一个 lua_State 中。
Lua 调用 C++ 时,C++ 函数挂起协程体:
在执行协程体的 Lua 脚本时,Lua 同样可以调用 C++ 的函数。C++ 函数体内也可以进行协程挂起,通过 lua_yieldk
便可以进行达到协程挂起。
五、lua_yieldk
LUA_API int (lua_yieldk) (lua_State *L, int nresults, lua_KContext ctx, lua_KFunction k);
描述:
将正在运行的协程体立即挂起。
当协程恢复运行时,控制权会直接交给延续函数 k 。当协程交出控制后,调用 lua_yield
的函数后续语句就不会被执行了。
参数:
- 参数 L:Lua State 的指针。
- 参数 nresults:将要返回给对应的
lua_resume
的栈中值的个数。 - 参数 ctx:传递给延续的上下文信息。
- 参数 k:延续函数。
值得注意:
如果 C++ 函数交出控制权之后,无需做后续处理,即挂起再恢复后,无需执行后续操作,可以不设置参数 k (设置为 NULL)。具体代码如下所示:
#define lua_yield(L,n) lua_yieldk(L, (n), 0, NULL)
六、C++ 与 Lua 协程交互的例子
- 创建一个新线程 lua_State ,在这个新线程中执行协程体。
- 协程体内,首先会在 Lua 脚本中挂起,并返回数据到宿主中。
- 恢复协程体后,Lua 从挂起点继续运行,调用 C++ 函数,C++ 函数内挂起协程体,并返回数据到宿主。
- 再次恢复协程后,运行 C++ 函数挂起点的延续函数,运行完延续函数,回至 Lua 调用 C++ 调用点继续执行至结束,返回数据到宿主。
第一步,编写 C++ 函数,暴露给 Lua 调用。
// 延续函数 int cfooK(lua_State *L, int status, lua_KContext ctx) { printf("恢复协程体后,调用 C++ 延续函数\n"); return 1; } // 暴露给 lua 调用的 C++ 函数 int primCFunction(lua_State *L) { lua_pushstring(L, "调用 C++ 函数,会进行挂起"); // 设置了 cfooK 作为延续函数,恢复协程之后,会进入 cfook 这一延续函数 lua_yieldk(L, 1, 0, &cfooK); // 这两种的使用结果是一样的,都不舍之延续函数 // lua_yieldk(L, 1, 0, nullptr); // lua_yield(L, 1); // 不会被执行,恢复之后会运行延续函数,不会执行后续的语句 printf("挂起之后的输(不会被输出)"); return 1; }
第二步,编写 Lua 脚本文件。
function foo(x) -- 协程挂起,并返回两个数据 coroutine.yield(10, x) end function foo1(x) foo(x + 1) primCFunction() return 3 end
第三步,最后编写运行代码。
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、线程和状态/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); // 返回值个数 int result = 0; // 获取 L1 中的 lua 文件的函数 foo1 lua_getglobal(L1, "foo1"); // 压入 integer lua_pushinteger(L1, 20); printf("第一次调用,Lua 脚本中挂起:\n"); auto state = lua_resume(L1, L, 1, &result); printf("协程状态: %s\n", getResumeState(state).c_str()); printf("L1 栈深度: %d\n", lua_gettop(L1)); printf("返回值个数: %d\n", result); printf("------------ L1 栈内容:------------\n"); stackDump(L1); printf("第二次调用,Lua 调用 C++ ,C++ 中挂起:\n"); state = lua_resume(L1, L, 0, &result); printf("协程状态: %s\n", getResumeState(state).c_str()); printf("L1 栈深度: %d\n", lua_gettop(L1)); printf("返回值个数: %d\n", result); printf("------------ L1 栈内容:------------\n"); stackDump(L1); printf("第三次调用,运行 C++ 延续函数,协程体结束::\n"); state = lua_resume(L1, L, 0, &result); printf("协程状态: %s\n", getResumeState(state).c_str()); printf("L1 栈深度: %d\n", lua_gettop(L1)); printf("返回值个数: %d\n", result); printf("------------ L1 栈内容:------------\n"); stackDump(L1); lua_close(L); }
最后运行的结果如下,这里就不再一个个步骤拆解了,代码有详细的注释,结合运行的结果便可分析出流转的过程了。
LUA_YIELD=1 LUA_OK=0 第一次调用,Lua 脚本中挂起: 协程状态: LUA_YIELD L1 栈深度: 2 返回值个数: 2 ------------ L1 栈内容:------------ 栈顶 ^ typename: number, value(integer): 21 ^ typename: number, value(integer): 10 栈底 第二次调用,Lua 调用 C++ ,C++ 中挂起: 协程状态: LUA_YIELD L1 栈深度: 1 返回值个数: 1 ------------ L1 栈内容:------------ 栈顶 ^ typename: string, value: '调用 C++ 函数,会进行挂起' 栈底 第三次调用,运行 C++ 延续函数,协程体结束:: 协程状态: LUA_OK L1 栈深度: 1 返回值个数: 1 ------------ L1 栈内容:------------ 栈顶 ^ typename: number, value(integer): 3 栈底
七、lua_xmove
LUA_API void (lua_xmove) (lua_State *from, lua_State *to, int n);
描述:
交换同一个状态机下不同线程中的值,会从 from 的栈上弹出 n 个值, 然后把它们压入 to 的栈上。
举个例子:
- 创建一个 lua_State "L",然后从这一 lua_State 创建一个新的线程 lua_State "L1"。
- 通过 L1 执行 Lua 脚本,得到两个数据。
- 将 L1 的数据移动到 L 中,打印结果。
第一步,编写 lua 脚本。
function foo(x) return "江澎涌", x * x end
第二步,编写主函数。
void copyStackElement() { lua_State *L = luaL_newstate(); luaL_openlibs(L); lua_State *L1 = lua_newthread(L); // 新线程中执行 Lua 脚本 std::string fname = PROJECT_PATH + "/12、线程和状态/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 文件的函数 foo lua_getglobal(L1, "foo"); // 压入 integer lua_pushinteger(L1, 5); // 调用函数 foo(5) -> "江澎涌", 25 lua_call(L1, 1, 2); printf("------------ 主线程 L 栈内容(xmove 前):--------------\n"); stackDump(L); printf("------------ 新线程 L1 栈内容(xmove 前):--------------\n"); stackDump(L1); // 从 L1 中拷贝 1 个元素到 L 中(会将 L1 元素弹出) lua_xmove(L1, L, 2); printf("------------ 主线程 L 栈内容(xmove 后):--------------\n"); stackDump(L); printf("------------ 新线程 L1 栈内容(xmove 后):--------------\n"); stackDump(L1); }
八、写在最后
Lua 项目地址:Github传送门
以上就是C++与Lua协程交互的示例详解的详细内容,更多关于C++与Lua协程交互的资料请关注脚本之家其它相关文章!
最新评论