« 地址对齐问题引起的 Bug 一则 | 返回首页 | MMORPG 中场景服务的抽象 »

Lua 下实现抢占式多线程

Lua 5.2 的开发进度可以回溯到 2010 年 1 月。漫长的流程到今天已经快两年过去,终于等到了 beta 版。我十分期待它可以在 2011 年内正式发布。在这几经折腾的两年里,许多新特性企图挤进 5.2 版,又最终被否决。

当我们审视改进列表,似乎看不到太多耳目一新的东西。但如果仔细阅读一下源代码,就会发现,大部分地方都重新实现过了,以配合这些表面上看起来不大的修改。如果你对 Lua 有足够理解,会发现,这次最激动人心的改进是 "yieldable pcall and metamethods" 。官方也把之列为 Main changes 第一条。语言上的重大新特性 goto 却被列在末尾。

当然,这只是我粗浅的理解而已。没有经过实践使用 5.2 一段时间,下这样的论断有点太草率。不过我还是想谈谈,这点改进可以给我们的开发带来什么。

coroutine 的 yield 现在几乎可以在任何地方使用了。我用了几乎,是因为它依然有一些限制。这些限制不大容易说的很清楚,为了理解其限制,我花了一整天实现阅读 lua 5.2 beta 版的源代码。这个话题下次有机会我再另写一篇 blog 总结一下。今天只谈应用。

我们知道 coroutine 可以实现一个协同多线程模型。即,每个线程(coroutine) 只在用户期望的地方跳出来,并可以在以后跳回去(保持当初跳离的状态)。这解决了许多抢占式多线程的麻烦。lua 的发明人在一篇访谈中谈到了 coroutine 解决并发的问题。但是,让用户不厌其烦的写 yield 是件很讨厌的事情。往往框架会把 yield 调用藏起来。如果我没记错,我读过的早期的 kepler 框架就是把 yield 藏在 html 的输出里的。能够想到的更漂亮的做法是编写一个 lua debug hook ,在 hook 里调用 yield 。这样就可以让 lua vm 每跑几行 lua byte code 就自动做 yield 一次。

可惜的是,lua 5.1 以前的版本不支持这个。因为 yield 限制太多了。如果恰巧 yield 发生在一次 metamethod 调用内部,或 pcall 内部,就会失败。这和 C/Lua 函数嵌套有关。lua vm 在实现 pcall 及 metamethod 机制时,不断的在 C 函数以及 lua 函数间跳转。一旦 yield 发生在多层 C 函数与 lua 函数嵌套调用的内部。用 longjmp 返回的 yield 机制,会丢失 C 函数调用的 stack frame 。也就是说,lua vm 本身的状态可以保留在 L 中,但 C 函数的状态却丢失了,无法正确返回。Lua 5.2 为了解决这个问题,引入了新的 api ,有兴趣可以阅读新的文档的 4.7 – Handling Yields in C 。不过真的想全部搞清楚,还是推荐阅读一下源代码。

只使用系统自带的 debug 库是不够的。用 lua 的 debug.sethook 设置一个 lua 函数做 hook ,里面依然不能调用 coroutine.yield 。这还是受到了 lua 实现的限制。正确的方法是直接使用 C api lua_sethook,把这样一个 hook 函数设进去:

static void
hook(lua_State *L, lua_Debug *ar)
{
    lua_yield(L,0);
}

至于你想让它每隔几行 lua 代码 yield 一次,还是让它发生在函数调用的时候,这看你的喜好。但一旦设置成功,一段 lua 代码就可以透明的定期 yield 出来了。

这些有什么用?我设想了两种用法,

一、是用 lua 做一个调度器,模拟出一个抢占式的多线程库。本质上,这个库是基于 coroutine 实现的。但切换线程是利用的 debug hook 定期强制切换。基于这个库,可以把以前我写过的这个小玩意改进一下。做的更易用一些。

二、是在一个 os 进程内启动多个独立的 lua state ,每个 lua state 并不包含多个 coroutine ,而只用一个 main thread 。设置 debug hook ,让 main thread 可以每跑一个阶段就 yield 出来,把控制权交还到上层的 C 代码。在 C 层次写一个有效的调度器。对 lua state 来说,每个都是独立的,它们之间可以通过 zeromq 这样的库通讯。lua state 还可以部署到多个 os thread 上,实现一个 M:N 的线程模型。它的调度器会比 os 来的更为高效。(关于在语言级实现 M:N 线程模型和 OS 级实现 M:N 线程序模型的性能差异,上个月 7 月 12 日我们在 google+ 上做过一次讨论。可惜是在有限圈子里做的,暂时没找到方法公开)且少占用大量的堆栈资源。

话说到这里,再看看,其实这不就是 Erlang 的模型么?:)

ps. 其实 lua 的早期版本也可以通过 lua coco 实现无限制的 yield 操作。但 coco 使用了 OS 的 fiber 库 这比 5.2 版的 lua 实现多出了额外的堆栈开销。


一些还没实现的想法:游戏服务器里的 npc ai 等东西是不是可以放在一个个独立的 lua state 中完成?它们相互不影响,用消息交互。可以一开始分配好一个 state 池,用来动态绑定新的 AI 单位(减少启动初始化的代价)。把任务切分成独立的小单位,在独立的 lua state 中做,我想对 lua 的 gc 操作也是极为有利的。

Comments

一直做前端开发,其实对多线程并不敏感,一般的事情已经足够跑了。现在不一样了:一来使用的Silverlight技术本身就是多线程,二来现在多核电脑已经普遍了。开始入手到里面,才发现水很深。今天终于在这里找到有启发的东西,非常感谢,希望以后能得到更多指教。
请教大师,Lua里面没有continue,怎么实现类似功能呢?
两年前我们做watchtower(http://blog.codingnow.com/2009/09/aoi_watchtower.html)的那段时间,似乎就是用的weak key来做到非侵入地给一个已有对象增加成员。 其实是错的!会导致内存泄漏!!! lua 5.2对weak key的定义变成了ephemeron table,才能用当年的那个办法。
大多数项目还是把lua当成一门过程式语言来用的。全局变量遍布,函数处处副作用。这种情况下要做调度器,做透明的yield,就必定造成各种状态变量错乱,根本不可能保证数据一致性。 文中第二种做法靠谱。要做erlang的模型,必须先限制变量的使用写无副作用的函数。(通过隔离lua_State强制要求)

Post a comment

非这个主题相关的留言请到:留言本