.. SPDX-License-Identifier: GFDL-1.3-or-later 关于Fk在同一个Lua中运行多个游戏房间的思考 ========================================= 目前Fk的实现中,每个游戏房间是一根线程,这就出现了一个问题: 当同时游戏的房间很多的时候,会给RAM带来相当大的负担,毕竟lua_State太多了, 每个Room都有一个。 目前比较流行的模式是只需三人就可进行的斗地主, 有时候同时运行着30多桌斗地主,直接把内存推向1GB占用,云服务器那才多少点资源。 如果这么多房间都在同一个Lua运行的话,情况应该会有很大改观。 多个房间如何调度? ------------------ 在当前阶段已经实现了重连和观战机制,这是借助于协程机制实现的。 当Lua正在执行delay或者等待用户答复时,Lua会从主协程中切换出来, 然后去另一个协程处理诸如旁观、重连等游戏逻辑之外的请求。 如果把这种空白时间更加充分的利用起来的话,或许就能同时执行多个房间了。 现在每个游戏房间在Lua中都是一个Room对象的实例,而Room 本身是通过一个协程执行着主游戏逻辑。 考虑修改一下,使得同一个Lua中能执行多个Room协程。 暂且考虑开个数组保存所有正在运行中的房间。 就绪队列 -------- 调度器除了维护运行中房间的数组之外,还维护一个就绪队列。 当房间不处于阻塞状态时,或者已经可以脱离阻塞状态,那么就认为他就绪。 此外还有一个特殊的协程,他用来处理托管、旁观等请求。 当他的这个请求队列为空的时候,也视为他未就绪。 .. note:: 所谓阻塞状态目前指的是正在delay或者正在等候答复时。自然而然的, 脱离阻塞状态就是delay的时间已经结束或者已收到答复。 当所有房间全部未就绪时,调度器调用sleep睡一段时间,让出CPU。 当房间因为delay而延时时,可以知道他恢复就绪的确切等待时间。但如果是等待答复、 等待新请求这种依赖玩家操作的协程,其就绪时间就完全不可预测了。 正是这种不可预测的等待时间才使得我们调度器只能小睡个几毫秒。 如何多睡一段时间? ------------------ 假设有多个房处于delay中,那么能睡的最长时间就是其中所有delay剩余时间的最小值。 但问题在于那些不可预测的就绪时间。如果另一个线程能告诉Lua答复已经就绪的话, 那么睡眠问题就好解决的多了。 实际上,可以借助信号量机制。当调度器开始睡觉时,肯定是调用了cpp函数。 这个cpp函数里面先将一个信号量置为0,然后再tryAcquire这个信号量一段时间。 当收到答复/收到请求时,把这个信号量+1。这样调度器就知道自己该醒过来啦。 如何避免房间等待太久? ---------------------- 目前的调度思路如下: - 若就绪队列为空,那么从所有运行中的房间进行一遍筛选。 - 若还为空,再筛选。 - 假如第二遍还空,那就睡觉。 - 假如就绪队列不空,那么弹出第一个,然后把控制权交给这个协程,等他自己让出。 在这个过程中,由于有些房间在等待就绪队列变空的过程中满足了就绪的条件, 那么再次筛选时,有可能重复的房间又会再次位于就绪队列靠前的位置。 这虽然不会导致某个房间永久等待下去的情况,但却可能让一个房间等待太长时间, 那我就又要被艾特了。 一种或许可行的解法是,在再次刷新队列的时候,优先选出上次队列里面没有的房间。 不知道这样如何呢? 怎么把现有的改成这样? ---------------------- CPP代码方面,首先Room已经不再是一个线程了,自然不能再当QThread的子类。 另外开个什么类来占据线程吧。 既然不是QThread的子类的话,那Room::run就得改了。原本是启动线程,现在还是改成 pushRequest吧。新请求立刻唤醒调度器,然后调度器切到请求处理协程处理新房间请求, 然后就开战! 既然要pushRequest,那么原先的请求队列也要从Room中移走了,这个得直接归属于线程。 当然,lua_State也要从Room撤走。 Lua代码方面,首先由于Engine成了所有房间共用的变量,必须杜绝所有对Engine 内变量的修改。点名cost_data和card.mark,这两个东西得通过__index大法托管给 Room挂着。剩下的没啥好说的,当然还有实现调度器。 协程池 ------ 虽然题文无关,但是FK确实在运行过程中不断的产生着协程。 为了避免因为不断产生新的协程导致的开销,可以使用协程池来管理。 用完的协程就存在某个数组留待下次使用。当需要启动一个新协程的时候, 先从协程池找,没有的话就新建一个。