Lua 的多线程支持
单个 Lua 虚拟机只能工作在一个线程下,如果你需要在同一个进程中让 Lua 并行处理一些事务,必须为每个线程部署独立的 Lua 虚拟机。
ps. 在少量多线程应用环境,加锁也是可行的。你可以在编译时自定义 lua_lock(L)
和 lua_unlock(L)
去调用操作系统的锁。
比较成熟的 lua 多线程库有 Lanes 和 Effil 。它们都试图隐藏多虚拟机的细节,让用户使用起来好像多线程在使用同一个虚拟机一样。比如 Effil 就用了 effil.table 去模拟 table 并让多个虚拟机可以共享数据;Lanes 则有 deep userdata 可以在不同线程间共享。
这些个方案都允许用户在不同的虚拟机间相互调用函数,大约是利用在虚拟机间同步函数的字节码和 upvalue 实现的。基于这些线程库的程序,用起来和别的支持多线程的语言(比如 Golang)那样没有太大区别。
但我不喜欢这种多线程解决方案。我认为多线程本身是复杂的,隐藏线程并行运行的细节,让用户基于共享状态去写程序并没有什么好处。如果阅读代码的人一眼看上去并不能立刻分辨出一个函数到底会在哪个线程运行,只会增加多线程程序的维护成本。
所以,skynet 采用的是让框架去支持多线程,让用户明确知道有独立的虚拟机的概念,以及它们跑在不同的线程下。只用消息队列来协同工作。还有很多类似的项目也是这样做的,比如 cqueues 。
我最近在开发客户端引擎时也遇到了多线程问题。例如 bgfx 的 log 回调就可能发生在不同线程中,所以无法简单的封装成 lua 函数进行回调;我们的引擎需要通过网络加载资源/代码,这部分网络 IO 处理最好能和逻辑线程分离,等等。
一开始,我采用的是 Lanes 。但是做了一段时间后,我觉得滥用多线程很容易滋生 bug 。
最近,我希望去掉 Lanes 这个库,改用自己实现的一套最简的线程库。由于客户端线程数量比较固定,无非是渲染线程,逻辑线程,IO 线程,Lua 调试器线程,物理线程;线程之间有固定的消息管道交换数据就够了。
所以,我想我可以从最基本的线程 api 设计起,一点点按需完善这个线程库。一开始,只需要有线程的创建;通讯管道可以支持收发 lua 基础数据类型就可用了。这部分在 skynet 中已经非常稳定,只需要把代码搬过来。
由于图形客户端有天然的运行周期(按帧渲染),我甚至不需要给通讯管道加读取超时参数。只用周期性查询是否有新数据即可。通讯管道也是有限的,所以并不需要像 golang 那样把 channel 做成 first class 的类型,并支持自由创建和销毁。我可以支持有限数量的具名管道,不同线程间约定好名字就可以通讯。
这些基础设施实现出来比我想象的要简单。搬运一些老代码,添加好 lua 的封装,一个周末就完成了。下周可以基于它们来重构我们已经做好(但还有 bug )的引擎代码。
Comments
Posted by: Casimodo | (4) August 17, 2020 06:51 PM
Posted by: Anonymous | (3) December 24, 2018 04:05 PM
Posted by: Anonymous | (2) November 20, 2018 09:44 PM
Posted by: anders0913 | (1) November 7, 2018 01:47 PM