C 语言

关注公众号 jb51net

关闭
首页 > 软件编程 > C 语言 > C++与Lua协程交互

C++与Lua协程交互的示例详解

作者:江澎涌

Lua 语言不支持真正的多线程,即不支持共享内存的抢占式线程,在执行协程体的 Lua 脚本时,Lua 同样可以调用 C++ 的函数,本文给大家介绍了C++ 与 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);

举个例子:

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_resumelua_pcall 的不同点:

参数:

返回值:

返回当前协程体处于哪个状态:

值传递:

lua_Stack 的栈是相互独立,所以此时如果需要将协程产出的数据传递到另一个 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 的函数后续语句就不会被执行了。

参数:

值得注意:

如果 C++ 函数交出控制权之后,无需做后续处理,即挂起再恢复后,无需执行后续操作,可以不设置参数 k (设置为 NULL)。具体代码如下所示:

#define lua_yield(L,n)		lua_yieldk(L, (n), 0, NULL)

六、C++ 与 Lua 协程交互的例子

第一步,编写 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 脚本。

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协程交互的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文