当前位置: BOLT界面引擎 > 知识库文章 > XLLuaRuntime的线程安全问题

XLLuaRuntime的线程安全问题

作者:李亚星

 

         关于一些关于在多线程里面不正确使用lua,会给程序带来一些隐患,这里说明一下XLLuaRuntime在多线程时候的使用注意事项

 

1.       目前使用lua,基本上都是通过XLLuaRuntime来托管的,XLLuaRuntime里面有:

l  lua环境 XL_LRT_ENV_HANDLE

l  lua运行时 XL_LRT_RUNTIME_HANDLE

l  lua_state

 

其中XLLuaRuntime可以有一个或者多个lua环境XL_LRT_ENV_HANDLE,每个lua 环境里面,又可以有一个或者多个lua运行时XL_LRT_RUNTIME_HANDLE,每个XL_LRT_RUNTIME_HANDLE和一个lua_state对应。

 

Lua环境由管理器EnvManager来管理,目前该管理器是线程安全的,也就是创建/销毁/查询一个lua环境,都是线程安全的操作,包括下面的接口函数

 

XLLRT_CreateEnv

XLLRT_DestroyEnv

XLLRT_GetEnv

 

需要注意的是,XL_LRT_ENV_HANDLE目前是以线程为边界的,也就是说XL_LRT_ENV_HANDLE不可以跨线程使用,你可以在A线程里面创建一个XL_LRT_ENV_HANDLE EnvA,也可以在B线程里面创建一个XL_LRT_ENV_HANDLE ENVB,但是一个lua环境在创建了之后,就只能在被创建的线程里面使用,不可以再跨越线程使用了,就上面的例子来说,EnvA只能在A线程里面使用,EnvB只能在B线程里面使用。

 

每个lua环境,可以创建一个或者多个lua运行时XL_LRT_RUNTIME_HANDLE,同理,这些运行时也只能在lua环境所属的线程里面使用,不可以跨线程使用,对应的lua运行时的接口,也都不是线程安全的。

 

每个lua运行时,又对应一个lua_state,该lua_state运用的范围就很广泛了,所有的lua封装里面对lua api的操作,都要依赖该lua_state,所以这里也需要特别注意,lua_state只能在对应的lua运行时所在的线程里面使用,绝不可以跨越线程使用,一方面lua api都不是线程安全的,比如lua_pushstring lua_pcall等,另一方面因为每个lua_state里面都共享了很多数据结构,比如每个lua_state共享一个堆栈,假如在多个线程里面同时在使用该lua_state,那么即使lua api是线程安全的,仍然会有隐患,比如我们在调用一个lua函数之前,先要通过lua_pushxxx,将参数压栈,然后在调用lua_pcall,等返回后,再从栈上面获取返回值,比如下面的例子

 

lua_pushstring(luaState,”abc”)

lua_pushinteger (luaStata,”0”)

lua_pcall(xxx)

lua_tostring(luaState, -1)

 

假如两个线程里面同时在对同一个luaState做类似的操作,那么在没有对整个代码段加锁保护之前,你就没有办法保证你调用lua_pcall时候,栈里面放的就是你想要的两个参数了,有可能已经被其它线程已经改掉了,导致种种诡异的问题出现。假如对所有这种代码段都加锁保护起来,一方面有效率问题,另一方面会导致lua封装的复杂度增加,要非常小心的处理每一段lua封装代码,防止出现竞争或者死锁,所以现在禁止在多个线程里面操作同一个lua_state

 

2.       lua环境里面注册进来的一些全局函数/全局对象/class的线程安全问题,我们下面统称扩展接口

 

目前可以注册这些的扩展接口有下面几个:

 

XLLRT_RegisterGlobalAPI

XLLRT_RemoveGlobalAPI

XLLRT_RegisterGlobalObj

XLLRT_RemoveGlobalObj

XLLRT_RegisterClass

XLLRT_UnRegisterClass

XLLRT_DoRegisterClass

 

按照前面所述,每个lua 环境只能运行在一个线程里面,那么假如你实现了一个或者几个扩展接口,并且这些扩展接口只是注册到一个lua环境里面,那么就很简单了,这些扩展接口都不需要考虑线程安全的问题,因为对这些接口的调用,只能在一个线程(注册到的lua环境所在的线程)里面调用,永远是线程安全的。

 

假如你把某个扩展接口,同时注册到了两个或者多个lua环境里面,这些就要小心了!需要看看这些lua环境是否是属于同一个线程的,假如都是属于同一个线程的,那么你同样可以放心了,不用考虑线程安全问题;假如属于不止一个线程,那么就需要注意了,这些扩展接口会被几个线程同时调用,所以你在实现这些扩展接口的时候,就需要保证这些扩展接口是线程安全的了,假如这些扩展接口用到了全局共享数据,那么就需要保护起来,假如用到了C运行库,那么要保证使用的是线程安全那份,假如用到了操作系统的api,要看看这些api是不是线程安全的,假如用到了com相关东西,那么就需要保证对应的com接口可以运行在MTA或者都是用同个STA内的接口等等,实现起来需要格外小心,确保不会发生竞争和死锁。

 

从上面来看,

1)         假如某个扩展接口非常单纯并且比较简单,很容易实现线程安全,那么自然可以被注册到位于不同线程的lua环境里面去,比如迅雷7里面的OSShell,目前来看就是线程安全的,可以被多个lua环境安全的使用

 

2)         但是对于多数接口,内部实现很复杂,牵扯到的功能也非常多,所以都不是线程安全的,只能在同一个线程里面使用,比如XLUE的所有接口目前都不是线程安全的,所以XLUE的相关接口,目前在使用的时候,都是注册到一个lua环境里面(使用XLUE的时候,启动时候会先创建第一个env并调用一些注册接口完成注册,该env一般位于程序的主线程里面,并且是全局默认的env

 

3)         假如你在有多个lua环境,并且位于不同的线程,那么在向这些env里面注册扩展接口的时候,需要考虑了:该接口是不是只注册到一个env里面?假如注册到了多个env里面,那么这些env是不是运行在同一个线程?假如存在多线程,那么该扩展接口是不是线程安全的?

 

3.       关于同一个lua环境里面多个lua运行时的问题

 

根据我们上面所说的,在同一个luaState不可以在多个线程里面使用,但是每个线程里面各使用一个luaState是没问题的(但是需要注意链接的c库等),也就是说对于一个luaState,只要自始至终都在同一个线程里面运行,那么就不会有线程安全的问题。

 

每个lua环境里面可以创建一个或者多个lua运行时(XL_LRT_RUNTIME_HANDLE),而每个lua运行时分别对应一个luaState,在理论上来说,只要lua环境本身是线程安全就可以了,这样对于用一个env,可以在不同线程里面各自创建一个属于自己线程的lua运行时,并且该lua运行时不会跨线程使用,那么就不会有问题啊,这里面关键的问题在于前面第二点所述的扩展接口的问题了

 

目前的扩展接口,是注册到env里面的,并不是注册到lua运行时里面的,也就是说,对于同一个env里面的多个lua运行时,都是共享同一份扩展接口(根据lua运行时的权限不同,可能导致会有不同的扩展接口,但是从本质上来说,还是共享了一份),每次创建一个新的运行时,都会向这个运行时对应的luaState里面注册所属env的适当权限的扩展接口。所以又回到了扩展接口的线程安全问题上面了,由于注册到该env里面的扩展接口,并没有做线程安全的限制,那么假如允许同一个env下面的多个lua运行时处于不同的线程,那么会导致很多问题的出现;假如要求所有的扩展接口都是线程安全的,又是不现实的,所以目前禁止了同一个env下面多个运行时的跨线程使用,也就是说目前的现实,只允许一个lua环境,自始至终在一个线程里面使用。

 

不过以后有没有可能让同一个env下面的lua运行时支持多线程呢?在luaState层面考虑,是支持的,因为每个lua运行时都有自己独立的luaState,但是最困难的部分就是所属lua环境的扩展接口的问题,如何保证扩展接口的线程安全?可以考虑有下面几种方案:

 

1)         规定所有注册进来的扩展接口都是线程安全的(不过看起来不现实,有些接口想实现线程安全极其困难)

 

2)         可以模拟com套间机制,在注册扩展接口时候,增加一个标识符,表示该接口是线程安全的,还是非线程安全的,假如该接口是线程安全的,那么可以直接被每个lua运行时使用了,不同做任何处理;假如是非线程安全的,那么需要XLLuaRuntime做序列化处理了,也就是类似com里面的STA,注册该接口时候所在的线程,就是该接口的所属线程,所有非所属程里面对该接口发起的调用,XLLuaRuntime自动对该调用做序列化处理,并交由所属线程发起真正调用。这里的序列化操作相对来说比较单纯,只需要对调用栈里面对应的参数和返回值做处理就可以。

 

上面的做法看似简单,但是也有很多问题存在,比如参数里面又有扩展接口该怎么办,并且会带来各种隐晦的问题,所以暂时应该不会使用。

 

4.       关于lua里面支持的coroutine 协同程序

 

需要注意的是,协同程序并不是真正的线程,虽然同一个luaState可以同时存在多个协同程序,并且每个协同程序有自己的堆栈,局部变量和指令指针,和线程类似,但是有根本上的不同,可以简单总结一下区别:(下面所述的多个协同程序,均属于同一个luaState的)

 

l  同一个luaState的多个协同程序,是运行在luaState所在的线程上面的,从协同程序的角度看来,看不到操作系统的线程这个层面的东西;线程是操作系统层面的东西,当某个线程被切换出去以后,那么该线程里面运行的所有协同程序,都会停止执行了,但是对于这些协同程序来说,它们是全然不知到的。

l  在同一个时刻,只可以运行一个协同程序;而对于线程来说,同一个时刻可以运行多个线程

l  协同程序需要用户显示用来调度,挂起或者切换到某个协同程序,所以协同程序的切换是可以明确知道的;线程的调度是由操作系统来管理的,并且是不可预期的。

l  协同程序的切换,开销非常少,只需要保存几个指针等就可以了,完全由用户态的代码操作;线程的切换的开销远大于协同程序的开销,需要在用户态和内核态之间切换等等

 

总起来说,lua里面的协同程序,类似windows下面的纤程(FiberThread)的概念,协同程序的功能很强大,但是概念相对来说比较复杂,有兴趣的可以仔细研究一下

 

5.  目前XLUEXLLuaRuntime里面都加入了线程安全检测的代码,xlue的接口都是非线程安全的,XLLuaRuntime里面的函数,除了XLLRT_CreateEnv/XLLRT_DestroyEnv

XLLRT_GetEnv是线程安全的,其它的所有需要传入XL_LRT_ENV_HANDLEXL_LRT_RUNTIME_HANDLElua_State做为参数的接口,都不是线程安全的,只能在env所属的线程里面使用,如果跨越线程使用了,会弹出错误提示框(日志版本或者专门定制的带线程安全检测的版本)

 

 

6.       可以简单总结一下:

l  尽量不要在非主线程里面操作lua,所有涉及lua的操作尽量都在主线程里面完成

 

l  线程里面要做的事情,涉及到lua的,要尽量简单,不要操作界面,调用XLUE的扩展接口等,目前XLUE的所有接口都不是线程安全的,不能在非主线程里面调用

 

l  如果迫不得已要在线程里面操作lua,那么要为这个线程创建一个新的lua环境,并在里面注册这个线程需要的扩展接口,注意线程里面不能使用主线程的env

 

l  XL_LRT_ENV_HANDLE XL_LRT_RUNTIME_HANDLE Lua_State不可以跨线程使用,也不可以在一个线程里面使用完了,转到另外一个线程里面继续用

 

l  在线程里面的env注册的扩展接口,要么保证该接口只在这个线程里面使用,要么保证该扩展接口是线程安全的

 

l  Lua环境,lua运行时的引用计数问题,使用完毕后,要注意释放,防止内存泄漏

 

l  假如在DllMain里面做了XLLRT_XXX相关调用,那么注意判断DllMain被调用的reason,否则会引发线程里面的调用。

 

l  Lua封装里面,回调Lua函数时候,不要使用lua_calllua_pcall,统一改用XLLuaRuntime里面提供的接口XLLRT_LuaCall,并且注意调用栈的平衡,假如XLLRT_LuaCall的返回值个数为不零0或者有错误出现,调用返回之后,要清栈,可以在调用之前,在push参数之前,调用lua_gettop保存栈顶,调用返回之后,使用完返回值之后,调用lua_settop恢复之前的栈顶

 

 

迅雷公司 版权所有 Copyright 2003-2010 Thunder Inc.All Rights Reserved. 意见反馈:xl7doc@xunlei.com