加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 大数据 > 正文

Lua的线程和状态 及协程

发布时间:2020-12-14 21:53:47 所属栏目:大数据 来源:网络整理
导读:luaL_loadstring(L,"return coroutine.create(function() end)"); ? nCallResult = lua_pcall(L,1,0); 创建一个协程和lua_newthread创建一个线程一样,不过这个创建会在线程的堆栈上压入一个上面的function() end; lua_newthread只是创建一个线程,堆栈个数

luaL_loadstring(L,"return coroutine.create(function() end)");
? nCallResult = lua_pcall(L,1,0);

创建一个协程和lua_newthread创建一个线程一样,不过这个创建会在线程的堆栈上压入一个上面的function() end;
lua_newthread只是创建一个线程,堆栈个数为0,面上面的个数为1,而且是function,使用lua_gettop和lua_type可得到

Lua的线程和状态

那不是真的多线程

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

多个线程

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

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

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

lua_State *lua_newthread(L);

这个函数返回一个lua_State指针,表示新建的线程。它会将新线程作为一个类型为“thread”的值压入栈中。如果我们执行了:

int lua_resume,  narg);

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

  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代码:

);if (!L1{0;} lua_getglobal "Func1" lua_pushinteger // 运行这个协同程序// 这里返回LUA_YIELD bRet 1 cout <<"bRet:" endl// 打印L1栈中元素的个数"Element Num:" lua_gettop// 打印yield返回的两个值"Value 1:" lua_tointeger-2"Value 2:"// 再次启动协同程序// 这里返回0;

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

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

);

对于多线程编程,本身就是麻烦的问题,而这里枯燥的文字总结,也会没有效果,下面来一个简短的例子。先贴Lua代码,这段代码需要结合C代码一起看,否则就是云里雾里的。

// 判断环境表中JellyThink是否被设置了staticIsSet lua_getfield LUA_ENVIRONINDEX"JellyThink"lua_isnil printf"Not setn"// 没有被设置就挂起(!"Begin yieldn"// 被设置了,就取值,返回被设置的值"Resumed againn"// 设置JellThink的值 luaL_checkinteger// 设置到环境表中 lua_pushvalue lua_setfield}

当我在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状态的。相信我,我还会回来的。

总结

哦,这篇文章拖的时间够长的啊。由于最近项目紧,赶着上线,很忙啊,加班啊。又赶上中秋节,也没有太多的时间来写。这篇就这样的,对于Lua状态的总结,还是不够深刻,或者说,基本就没有。哦,算了,后续在总结吧,也不能,也不可能一口吃成一个胖子的。中秋快乐,各位。

2014年9月8日 于深圳。


int running = 1;?

int lua_finish(lua_State * L) {?
running = 0;?
printf("lua_finish calledn");?
return 0;?
}?
int lua_sleep(lua_State *L) {?
printf("lua_sleep calledn");?
//lua_pushnumber(L,10);?
return lua_yield(L,0);?
int main() {?
lua_State* L = luaL_newstate();?
luaL_openlibs(L);?
lua_register(L,"sleep",lua_sleep);?
//luaL_dofile(L,"scripts/init.lua");?
lua_State* cL = lua_newthread(L);?
luaL_loadfile(cL,"loop.lua");?
while (running) {?
int status;?
lua_pushnumber(cL,20);?
status = lua_resume(cL,1);?
if (status == LUA_YIELD) {?
printf("loop yieldingn");?
else {?
running = 0; // you can't try to resume if it didn't yield?
// catch any errors below?
if (status == LUA_ERRRUN && lua_isstring(cL,-1)) {?
printf("isstring: %sn",lua_tostring(cL,-1));?
lua_pop(cL,-1);?
lua_close(L);?

}


lua file

print("loop.lua")


local i = 0
while true do
? ? print("lua_loop iteration")
? ? local bret = sleep()
print(bret)


? ? i = i + 1
? ? if i == 4 then
? ? ? ? break
? ? end
end


finish()

 Lua中的协程和多线程很相似,每一个协程有自己的堆栈,自己的局部变量,可以通过yield-resume实现在协程间的切换。不同之处是:Lua协程是非抢占式的多线程,必须手动在不同的协程间切换,且同一时刻只能有一个协程在运行。并且Lua中的协程无法在外部将其停止,而且有可能导致程序阻塞。

?

协同程序(Coroutine):

  三个状态:suspended(挂起,协同刚创建完成时或者yield之后)、running(运行)、dead(函数走完后的状态,这时候不能再重新resume)。

  coroutine.create(arg):根据一个函数创建一个协同程序,参数为一个函数

  coroutine.resume(co):使协同从挂起变为运行(1)激活coroutine,也就是让协程函数开始运行;(2)唤醒yield,使挂起的协同接着上次的地方继续运行。该函数可以传入参数

  coroutine.status(co):查看协同状态

  coroutine.yield():使正在运行的协同挂起,可以传入参数

  resume函数的两种用途虽然都是使协同挂起,但还是有些许差异的,看下面这个例子:

coroutineFunc = function (a,b) 
    for i = 1,10 do
        print(i,a,b)
        coroutine.yield()
    end
end

co2 = coroutine.create(coroutineFunc)        --创建协同程序co2
coroutine.resume(co2,100,112)">200)                 1 100 200 开启协同,传入参数用于初始化
coroutine.resume(co2)                         2 100 200 
500,112)">600)                 3 100 200 继续协同,传入参数无效

co3 = 创建协同程序co3
coroutine.resume(co3,112)">300,112)">400)                 1 300 400 开启协同,传入参数用于初始化
coroutine.resume(co3)                         2 300 400 
 3 300 400 

?  Lua中协同的强大能力,还在于通过resume-yield来交换数据:

  (1)resume把参数传给程序(相当于函数的参数调用);

  (2)数据由yield传递给resume;

  (3)resume的参数传递给yield;

  (4)协同代码结束时的返回值,也会传给resume

?  协同中的参数传递形势很灵活,一定要注意区分,在启动coroutine的时候,resume的参数是传给主程序的;在唤醒yield的时候,参数是传递给yield的。看下面这个例子:

co = coroutine.create(function (a,b) print("co",b,coroutine.yield()) end)
coroutine.resume(co,12)        --没输出结果,注意两个数字参数是传递给函数的
34,5co 1 2 3 4 5,这里的两个数字参数由resume传递给yield 

  Lua的协同称为不对称协同(asymmetric coroutines),指“挂起一个正在执行的协同函数”与“使一个被挂起的协同再次执行的函数”是不同的,有些语言提供对称协同(symmetric coroutines),即使用同一个函数负责“执行与挂起间的状态切换”。

?  注意:resume运行在保护模式下,因此,如果协同程序内部存在错误,Lua并不会抛出错误,而是将错误返回给resume函数。

?  以下是我个人的一点理解:

  (1)resume可以理解为函数调用,并且可以传入参数,激活协同时,参数是传给程序的,唤醒yield时,参数是传递给yield的;

  (2)yield就相当于是一个特殊的return语句,只是它只是暂时性的返回(挂起),并且yield可以像return一样带有返回参数,这些参数是传递给resume的。

为了理解上面两句话的含义,我们来看一下如何利用Coroutine来解决生产者——消费者问题的简单实现:

produceFunc = function() while true do local value = io.read() print("produce: ",value) coroutine.yield(value) 返回生产的值 end consumer = function(p) while true status,255)">coroutine.resume(p); --唤醒生产者进行生产 print("consume: ",value) end 消费者驱动的设计,也就是消费者需要产品时找生产者请求,生产者完成生产后提供给消费者 producer = create(produceFunc) consumer(producer)

这是一种消费者驱动的设计,我们可以看到resume操作的结果是等待一个yield的返回,这很像普通的函数调用,有木有。我们还可以在生产消费环节之间加入一个中间处理的环节(过滤器):

  可以看到,我们在中间过滤器中将生产出的值放大了一百倍。

  通过这个例子应该很容易理解coroutine中如何利用resume-yield调用来进行值传递了,他们像“调用函数——返回值”一样的工作,也就是说resume像函数调用一样使用,yield像return语句一样使用。coroutine的灵活性也体现在这种通过resume-yield的值传递上。

#include <lua/lua.hpp> bool running =true;int lua_finish(lua_State *) {false printf("lua_finish calledn");return 0} lua_sleep*L)"lua_sleep calledn" lua_yield, main() lua_State L  lua_open(); luaL_openlibs lua_register"sleep""finish" luaL_dofile"scripts/init.lua" cL  lua_newthreadcL"scripts/loop.lua"whilerunning status status  lua_resumeifstatus == LUA_YIELD"loop yieldingn"else running= // you can't try to resume if it didn't yield// catch any errors below LUA_ERRRUN && lua_isstring-1))"isstring: %sn" lua_tostring)); lua_pop"scripts/end.lua" lua_close}

loop.lua

print"loop.lua"local i 0truedo"lua_loop iteration" sleep+14thenbreakend finish()

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读