Lua的线程和状态

【那不是真的多线程】

Lua不支持真正的多线程,这句话我在《Lua中的协同程序》这篇文章中就已经说了。根据个人编程经验,在开发过程当中,若是能够避免使用线程,那就坚定不用线程,若是实在没有更好的办法,那就只能退而用之。为何?首先,多个线程之间的通讯比较麻烦,同时,线程之间共享内存,对于共享资源的访问,使用都是一个很差控制的问题;其次,线程之间来回切换,也会致使一些不可预估的问题,对性能也是一种损耗。Lua不支持真正的多线程,而是一种协做式的多线程,彼此之间协做完成,并非抢占完成任务,因为这种协做式的线程,所以能够避免由不可预知的线程切换所带来的问题;另外一方面,Lua的多个状态之间不共享内存,这样便为Lua中的并发操做提供了良好的基础。html

【多个线程】

从C API的角度来看,将线程想象成一个栈可能更形象些。从实现的观点来看,一个线程的确就是一个栈。每一个栈都保留着一个线程中全部未完成的函数调用信息,这些信息包括调用的函数、每一个调用的参数和局部变量。也就是说,一个栈拥有一个线程得以继续运行的全部信息。所以,多个线程就意味着多个独立的栈。编程

当调用Lua C API中的大多数函数时,这些函数都做用于某个特定的栈。当咱们调用lua_pushnumber时,就会将数字压入一个栈中,那么Lua是如何知道该使用哪一个栈的呢?答案就在类型lua_State中。这些C API的第一个参数不只表示了一个Lua状态,还表示了一个记录在该状态中的线程。windows

只要建立一个Lua状态,Lua就会自动在这个状态中建立一个新线程,这个线程称为“主线程”。主线程永远不会被回收。当调用lua_close关闭状态时,它会随着状态一块儿释放。调用lua_newthread即可以在一个状态中建立其余的线程。多线程

lua_State *lua_newthread(lua_State *L);

这个函数返回一个lua_State指针,表示新建的线程。它会将新线程做为一个类型为“thread”的值压入栈中。若是咱们执行了:并发

L1 = lua_newthread(L);

如今,咱们拥有了两个线程L和L1,它们内部都引用了相同的Lua状态。每一个线程都有其本身的栈。新线程L1以一个空栈开始运行,老线程L的栈顶就是这个新线程。函数

除了主线程之外,其它线程和其它Lua对象同样都是垃圾回收的对象。当新建一个线程时,线程会压入栈,这样能确保新线程不会成为垃圾,而有的时候,你在处理栈中数据时,不经意间就把线程弹出栈了,而当你再次使用该线程时,可能致使找不到对应的线程而程序崩溃。为了不这种状况的发生,能够保持一个对线程的引用,好比在注册表中保存一个对线程的引用。性能

当拥有了一个线程之后,咱们就能够像主线程那样来使用它,之前博文中提到的对栈的操做,对这个新的线程都适用。然而,使用多线程的目的不是为了实现这些简单的功能,而是为了实现协同程序。ui

为了挂起某些协同程序的执行,并在稍后恢复执行,咱们可使用lua_resume函数来实现。lua

int lua_resume(lua_State *L, int narg);

lua_resume能够启动一个协同程序,它的用法就像lua_call同样。将待调用的函数压入栈中,并压入其参数,最后在调用lua_resume时传入参数的数量narg。这个行为与lua_pcall相似,但有3点不一样。spa

  1. lua_resume没有参数用于指出指望的结果数量,它老是返回被调用函数的全部结果;
  2. 它没有用于指定错误处理函数的参数,发生错误时不会展开栈,这就能够在发生错误后检查栈中的状况;
  3. 若是正在运行的函数交出(yield)了控制权,lua_resume就会返回一个特殊的代码LUA_YIELD,并将线程置于一个能够被再次恢复执行的状态。

当lua_resume返回LUA_YIELD时,线程的栈中只能看到交出控制权时所传递的那些值。调用lua_gettop则会返回这些值的数量。若要将这些值移到另外一个线程,可使用lua_xmove。

为了恢复一个挂起线程的执行,能够再次调用lua_resume。在这种调用中,Lua假设栈中全部的值都是由yield调用返回的,固然了,你也能够任意修改栈中的值。做为一个特例,若是在一个lua_resume返回后与再次调用lua_resume之间没有改变过线程栈中的内容,那么yield刚好返回它交出的值。若是能很好的理解这个特例是什么意思,那就说明你已经很是理解Lua中的协同程序了,若是你仍是不知道我说的这个特例是什么意思,请再去读一遍《Lua中的协同程序》,若是你还不懂,那你就在下放留言吧(提醒:这个特例主要利用的是resume-yield之间的传参规则)。

如今,我就经过一个简单的程序来作个试验,以便更好的理解Lua的线程。使用C代码来调用Lua脚本,Lua函数做为一个协同程序来启动,这个Lua函数能够调用其它Lua函数,任意的一个Lua函数均可以交出控制权,从而使lua_resume调用返回。对于使用C调用Lua不熟悉的伙计,请再去仔细的读读《Lua与C》和《C“控制”Lua》这两篇文章吧。先贴上重要的代码吧。下面是Lua代码:

function Func1(param1)
    Func2(param1 + 10)
    print("Func1 ended.")
    return 30
end

function Func2(value)
    coroutine.yield(10, value)
    print("Func2 ended.")
end

 

下面是C++代码:

lua_State *L1 = lua_newthread(L);
if (!L1)
{
    return 0;
}

lua_getglobal(L1, "Func1");
lua_pushinteger(L1, 10);

// 运行这个协同程序
// 这里返回LUA_YIELD
bRet = lua_resume(L1, 1);
cout << "bRet:" << bRet << endl;

// 打印L1栈中元素的个数
cout << "Element Num:" << lua_gettop(L1) << endl;

// 打印yield返回的两个值
cout << "Value 1:" << lua_tointeger(L1, -2) << endl;
cout << "Value 2:" << lua_tointeger(L1, -1) << endl;

// 再次启动协同程序
// 这里返回0
bRet = lua_resume(L1, 0);
cout << "bRet:" << bRet << endl;
cout << "Element Num:" << lua_gettop(L1) << endl;
cout << "Value 1:" << lua_tointeger(L1, -1) << endl;

上面的程序,你能够先运行一下;你能想到运行结果么?单击这里下载完整工程LuaThreadDemo.zip

上面的例子是C语言调用Lua代码,Lua能够本身挂起本身;若是Lua去调用C代码呢?C函数不能本身挂起它本身,一个C函数只有在返回时,才会交出控制权。所以C函数其实是不会中止自身执行的,不过它的调用者能够是一个Lua函数,那么这个C函数调用lua_yield,就能够挂起Lua调用者:

int lua_yield(lua_State *L, int nresults);

你没有听错,C代码调用lua_yield不能挂起本身,可是它却能够将它的Lua调用者挂起。其中nresults是准备返回给相应resume的栈顶值的个数,当协同程序再次恢复执行时,Lua调用者会收到传递给resume的值。lua_yield在使用时,只能做为一个返回的表达式,而不能独自使用。好比:

return lua_yield(L, 0);

对于多线程编程,自己就是麻烦的问题,而这里枯燥的文字总结,也会没有效果,下面来一个简短的例子。先贴Lua代码,这段代码须要结合C代码一块儿看,不然就是云里雾里的。

require "lua_yieldDemo"

local function1 = function ()
    local value
    repeat
      value = Module.Func1()
    until value
    return value
end

local thread1 = coroutine.create(function1)

-- 如今运行到了Module.Func1()
-- 100这个值将会被赋值给value
coroutine.resume(thread1)
--print(coroutine.status(thread1))

-- 设置C函数环境
Module.Func2(10)
print(coroutine.resume(thread1))

C代码以下:

// 判断环境表中JellyThink是否被设置了
static int IsSet(lua_State *L)
{
    lua_getfield(L, LUA_ENVIRONINDEX, "JellyThink");
    if (lua_isnil(L, -1))
    {
        printf("Not set\n");
        return 0;
    }
    return 1;
}

static int Func1(lua_State *L)
{
    // 没有被设置就挂起
    if (!IsSet(L))
    {
        printf("Begin yield\n");
        return lua_yield(L, 0);
    }
    
    // 被设置了,就取值,返回被设置的值
    printf("Resumed again\n");
    lua_getfield(L, LUA_ENVIRONINDEX, "JellyThink");
    return 1;
}

// 设置JellThink的值
static int Func2(lua_State *L)
{
    luaL_checkinteger(L, 1);

    // 设置到环境表中
    lua_pushvalue(L, 1);
    lua_setfield(L, LUA_ENVIRONINDEX, "JellyThink");
    return 0;
}

当我在Lua中调用coroutine.resume时,我都只传递了一个参数,其它参数都没有;这里须要注意,若是我传值了,就至关于给value赋值了。当我恢复thread1运行时,它是从Module.Func1()返回处继续执行,也就是对value赋值,而这里赋予value的值其实是传给resume的值。上面的代码中,我没有传值,若是传了,就没法验证我设置的10了。单击这里下载完整工程lua_yieldDemo.zip。Any question? No? OK, Next.

【Lua状态】

每次调用luaL_newstate(或者lua_newstate)都会建立一个新的Lua状态。不一样的Lua状态是各自彻底独立的,它们之间不共享任何数据。这个概念是否是很熟悉,是否是特别像Windows中的进程的概念。也就是说,在一个Lua状态中发生的错误也不会影响其它的的Lua状态,windows的进程也是这样的。而且,Lua状态之间不能直接沟通,必须写一些辅助代码来完成这点。

因为全部交换的数据必须经由C代码中转,因此只能在Lua状态间交换那些能够在C语言中表示的类型,例如字符串和数字。因为Lua状态我目前没有使用过,也就没有足够的信心和资格去总结这个东西,仍是怕会误导你们,若是之后在实际项目中使用了Lua状态,我还会回过头来总结Lua状态的。相信我,我还会回来的。

相关文章
相关标签/搜索