零、前语

Lua 言语不支持真正的多线程,即不支持同享内存的抢占式线程。

这样的模式能减少一些多线程的问题。多线程的问题源于线程抢占和同享内存,而假如非抢占式线程或者不运用同享内存则能防止多线程问题,Lua 同时支持这两种计划。从之前分享的《Lua 协程》文章中知道:

  1. Lua 言语的线程是协作式的,即协程,能够防止因不行预知的线程切换带来的问题。
  2. 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);
  1. 新线程 L1 与主线程 L 同享全局变量和注册表,但是它们具有独立的栈空间。新线程 L1 从空栈开端,主线程 L 则在其栈顶会引证 L1 这个新线程。
  2. 除了主线程 L 需求运用 lua_close 进行关闭,新线程 L1 和其他的 Lua 目标相同都是废物收回的目标。所以永久不要运用未被正确锚定在 lua_State 中的线程(主线程是内部锚定,因而不必忧虑会被收回),因为一切对 Lua API 的调用都有可能收回未锚定的线程,即使是正在运用这个线程进行函数调用。 要防止这种状况,应该在 “已锚定的线程的栈”、“注册表”、“Lua 变量” 中保留对运用中线程的引证,防止被废物收回机制收回。
  3. 具有的新线程 L1 能够像主线程 L 相同运用,进行压栈调用函数等操作。

举个比如:

  1. 创立一个新的线程。
  2. 分别打印各自栈的内容。
lua_State *L = luaL_newstate();
luaL_openlibs(L);
// 用 L 创立一个新的线程
// L 和 L1 各自有一个栈
// L 的栈顶是 L1 的 thread
// L1 的栈是空
lua_State *L1 = lua_newthread(L);
printf("主线程 L 栈深度:%dn", lua_gettop(L));
printf("主线程 L 栈内容:------------n");
stackDump(L);
printf("新线程 L1 栈深度:%dn", 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 的不同点:

  1. lua_resume 中没有表明期望成果数量的参数,总是回来被调用函数的一切成果。
  2. 没有过错处理函数的参数,发生过错时不会进行栈展开,能够在过错发生后查看栈的状况。
  3. 假如正在运转的函数被挂起,lua_resume 就会回来代码 LUA_YIELD ,并将线程置于一个后续能够康复履行的状况中。

参数:

  • 参数 L:Lua State 的指针。
  • 参数 from:正在履行调用的线程,或为 NULL 。
  • 参数 narg:入参参数数量。
  • 参数 nres:协程体回来的值数量。

回来值:

回来当时协程体处于哪个状况:

  1. 假如被挂起,则回来 LUA_YIELD
  2. 假如已经履行完成,则回来 LUA_OK
  3. 假如 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)。详细代码如下所示:

lua_yieldk(L, 1, 0, nullptr);

也能够运用 lua_yield 代替,他是一个宏,仅仅简便了写法。

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

六、C++ 与 Lua 协程交互的比如

  1. 创立一个新线程 lua_State ,在这个新线程中履行协程体。
  2. 协程体内,首要会在 Lua 脚本中挂起,并回来数据到宿主中。
  3. 康复协程体后,Lua 从挂起点持续运转,调用 C++ 函数,C++ 函数内挂起协程体,并回来数据到宿主。
  4. 再次康复协程后,运转 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=%dn", LUA_YIELD);
    printf("LUA_OK=%dn", 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("协程状况: %sn", getResumeState(state).c_str());
    printf("L1 栈深度: %dn", lua_gettop(L1));
    printf("回来值个数: %dn", result);
    printf("------------ L1 栈内容:------------n");
    stackDump(L1);
    printf("第2次调用,Lua 调用 C++ ,C++ 中挂起:n");
    state = lua_resume(L1, L, 0, &result);
    printf("协程状况: %sn", getResumeState(state).c_str());
    printf("L1 栈深度: %dn", lua_gettop(L1));
    printf("回来值个数: %dn", result);
    printf("------------ L1 栈内容:------------n");
    stackDump(L1);
    printf("第三次调用,运转 C++ 连续函数,协程体结束::n");
    state = lua_resume(L1, L, 0, &result);
    printf("协程状况: %sn", getResumeState(state).c_str());
    printf("L1 栈深度: %dn", lua_gettop(L1));
    printf("回来值个数: %dn", 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    
栈底
第2次调用,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 的栈上。

举个比如:

  1. 创立一个 lua_State “L”,然后从这一 lua_State 创立一个新的线程 lua_State “L1″。
  2. 经过 L1 履行 Lua 脚本,得到两个数据。
  3. 将 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);
}

最终输出成果,能够看到,L1 得到的成果迁移至 L 中,L1 的栈则被清空。

------------ 主线程 L 栈内容(xmove 前):--------------
栈顶
^ typename: thread, value: thread    
栈底
------------ 新线程 L1 栈内容(xmove 前):--------------
栈顶
^ typename: number, value(integer): 25    
^ typename: string, value: '江澎涌'    
栈底
------------ 主线程 L 栈内容(xmove 后):--------------
栈顶
^ typename: number, value(integer): 25    
^ typename: string, value: '江澎涌'    
^ typename: thread, value: thread    
栈底
------------ 新线程 L1 栈内容(xmove 后):--------------
栈顶
栈底

八、写在最终

Lua 项目地址Github传送门 (假如对你有所协助或喜爱的话,赏个star吧,码字不易,请多多支持)

假如觉得本篇博文对你有所启示或是解决了困惑,点个赞或重视我呀

大众号搜索 “江澎涌”,更多优质文章会第一时间分享与你。

C++ 与 Lua 的协程交互