线程和状态

本文详细介绍了Lua中的线程(协程)概念,强调了线程与栈的关联,以及如何通过lua_State类型管理线程。同时,讨论了Lua状态的独立性,以及在多线程环境下如何在不同Lua状态间通信。通过示例展示了如何创建、恢复和管理线程,以及在C API中使用lua_yieldk和lua_yield进行控制权转移。
摘要由CSDN通过智能技术生成

文章新地址

文章目录


  Lua语言不支持真正的多线程,即不支持共享内存的抢占式线程。原因有两个,其一是IOS C没有提供这样的功能,因此也没有可移植的方法能在Lua中实现这种机制:其二,也是更重要的原因,在于我们认为在Lua中引入多线程不是一个好主意。
  多线程一般用于底层编程。像信号量和监视器这样的同步机制一般都是操作系统上下文提供的,而非应用程序提供。要查找和纠正多线程相关的bug是很困难的,其中有些Bug还会导致安全隐患。此外,程序中的一些需要同步的临界区还可能由于同步而导致性能问题。
  多线程的这些问题源于线程抢占和共享内存,因此如果使用非抢先式的线程后者不使用共享内存就可以避免这些问题。Lua语言同时支持这两种方案。Lua语言的线程是协作式的,因此可以避免因不可预知的线程切换而带来的问题。另一方面,Lua状态之间不共享内存,因此也为Lua语言中实现并行化提供了良好基础。

多线程

  在Lua语言中,协程的本质就是线程。我们可以认为协程是带有良好编程接口的线程,也可以认为线程是带有底层API的协程。
  从C API的角度来看,把线程当作一个栈会比较有用;而从实现的角度来看,栈实际上就是线程。每个站都保存着一个线程中挂起的函数调用信息,外加每个函数调用的参数和局部变量。换句话说,一个栈包括了一个线程得以继续运行所需的所有信息。因此,多个线程就意味着多个独立的栈。
  Lua语言中 CAPI的大多数函数操作的特定的栈,Lua是如何知道应该使用哪个栈的呢?当调用lua_pushnumber时,是怎么制定将数字压入何处的呢?秘密在于lua_State类型,即这些函数的第一个参数,它不仅表示一个Lua状态,还表示带有该状态的一个线程。
  当创建一个Lua状态时,Lua就会自动用这个状态创建一个主线程,并返回代表该线程的lua_State。这个主线程永远不会被垃圾回收,它只会调用lua_close关闭状态时随着状态一起释放。与线程无关的程序会在这个主线程中运行所有的代码。
  调用lua_newthread可以在一个状态中创建其他的线程:

lua_State *lua_newthread (lua_State *L);

该函数会将新线程作为一个"thread"类型的值压入栈中,并返回一个表示新线程的lua_State类型的指针。例如,考虑如下的语句:

L1 = lua_newthread(L);

执行上述代码后,我们就有了两个线程L1和L,它们都在内部引用了相同的Lua状态。每个线程都有其自己的栈。新线程L1从空栈开始运行,而老线程L在其栈顶会引用这个新线程:

printf("%d\n",lua_gettop(L1));  -- 0
printf("%d\n",luaL_typename(L,-1));   -- thread

  除主线程以外,线程和其他的Lua对象一样都是垃圾回收的对象。当新建一个线程时,新创建的线程会被压入栈中,这样就保证了新线程不会被垃圾收集。永远不要使用未被正确锚定在Lua状态中的线程。所有对LuaAPI的调用都有可能回收未锚定的线程,即使是在正在使用这个线程的函数调用。例如,考虑如下的代码:

lua_State *L1 = lua_newthread(L);
lua_pop(L,1);
lua_pushstring(L1,"hello");

调用lua_pushstring可能会触发垃圾收集器并回收L1,从而导致应用崩溃,尽管L1正在被使用。要避免这种情况,应该在诸如一个已锚定线程的栈、注册表或Lua变量中保留一个对使用中线程的引用。
  一旦拥有一个新线程,我们就可以像使用主线程一样来使用它了。我们可以将元素压入栈中,或者从栈中弹出元素,还可以用它来调用函数等等。例如,如下代码在新线程中调用了f(5),然后将结果传递到老线程中:

lua_getglobal(L1,"f");  /* 假设'f'是一个全局函数 */
lua_pushinteger(L1,5);
lua_call(L1,1,1);
lua_xmove(L1,L,1);

函数lua_xmove可以在同一个Lua状态的两个栈之间移动Lua值。一个形如lua_xmove(F,T,n)的调用会从栈F中弹出n个元素,并将它们压入栈T中。
  不过,对于这类用法,我们不需要用新线程,用主线程就足够了。使用多线程的主要目的是实现协程,从而可以挂起某些协程的执行,并在之后恢复执行。因此,我们需要用到函数lua_resume:

int lua_resume (lua_State *L,lua_State *from, int narg);

  要启动一个协程,我们可以像使用lua_pcall一样使用lua_resume:将待调用函数压入栈,然后压入协程的参数,并以参数的数量作为参数narg调用lua_resume(参数from是正在执行调用的线程,或为NULL)。这个行为与lua_pcall类似,但有三个不同点。首先,lua_resume中没有表示期望结果数量的参数,它总是返回被调用函数的额所有结果。其次,它没有表示错误处理函数的参数,发生错误时不会进行栈展开,这样我们就可以在错误发生后检查栈的情况。最后,如果正在运行的哈数被挂起,lua_resume就会返回代码LUA_YIELD,并将线程置于一个可以后续再恢复执行的状态中。
  当lua_resume返回LUA_YIELD时,线程栈中的可见部分只包含传递给yield的值。调用lua_gettop会返回这些值的个数。如果要将这些值转移到另一个线程,可以使用lua_xmove。
  要恢复一个挂起的线程,可以再次调用lua_resume。在这种调用中,Lua假设栈中所有的值都会被调用的yield返回。例如,如果在一个lua_resume返回后到再次调用lua_resume时不改变线程的栈,那么yield会原样返回它产生的值。
  通常,我们会把一个Lua函数作为协程启动协程。这个Lua函数可以调用其他Lua函数,并且其中任意一个函数都可以挂起,从而结束对lua_resume的调用。例如,假设有如下定义:

function foo(x) coroutine.yield(10,x) end
function foo1(x) foo(x + 1); return 3 end

现在运行一下C语言代码:

lua_State *L1 = lua_newthread(L);
lua_getglobal(L1,"foo1");
lua_pushinteger(L1,20);
lua_Resume(L1,L,1);

调用lua_resume会返回LUA_YIELD,表示线程已交出了控制权。此时,L1的栈便有了位yield指定的值:

printf("%d\n",lua_gettpo(L1));			--> 2
printf("%lld\n",lua_tointeger(L1,1));   --> 10
printf("%lld\n",lua_tointeger(L1,2));   --> 21

当恢复此线程时,它会从挂起的地方(即调用yield的地方)继续执行。此时,foo会返回到foo1,foo1继而又返回到lua_resume:

lua_resume(L1,L,0);
printf("%d\n",lua_gettpo(L1));   --> 1
printf("%lld\n",lua_tointeger(L1,1));  -->3

第二次调用lua_resume是会返回NULL_OK,表示一个正常的返回。
  一个协程也可以调用C语言函数,而C语言函数又可以反过来调用其他Lua函数。我们已经讨论过如何使用延续来让这些Lua函数交出控制权。C语言函数也可以交出控制权。在这这种情况下,它必须提供一个在线恢复时被调用的延续函数。要交出控制权,C语言函数必须调用如下的函数:

int lua_yield (lua_State *L, int nresults, int ctx, lua_CFunction k);

在返回语句中我们应该始终使用这个函数,例如:

static inf myCfunction(lua_State *L){
   
	...
	return lua_yieldk(L,nreseults,ctx
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

平淡风云

您的打赏是我继续创作的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值