2.9 使用Skynet的注意事项
使用一套引擎,就要理解它的特性。Skynet最大的特性是“提供同一机器上充分利用多核CPU的处理能力”,由此带来的时序问题值得特别注意。
2.9.1 协程的作用
Skynet服务在收到消息时,会创建一个协程,在协程中会运行消息处理方法(即用skynet.dispatch设置的回调方法)。这意味着,如果在消息处理方法中调用阻塞API(如skynet.call、skynet.sleep、socket.read),服务不会被卡住(仅仅是处理消息的协程被卡住),执行效率得以提高,但程序的执行时序将得不到保证。
如图2-36所示,某个服务的消息队列存在多条消息,第一条消息的处理函数是OnMsg1,第二条是OnMsg2,OnMsg1调用了阻塞方法skynet.sleep。尽管程序会依次调用OnMsg1、OnMsg2……但当执行到阻塞函数时,协程会挂起。实际执行顺序可能是图2-36中右边展示的“语句1、skynet.sleep、语句3、语句4、语句2”。
图2-36 使用阻塞API需要注意时序问题
2.9.2 扣除金币的Bug
本节展示一种不注意时序问题导致的Bug,假设游戏有“存款”功能,玩家可以把一定数量的金币存入银行,获得利息。相关服务如图2-37所示,agent服务代表玩家控制的角色,bank服务代表银行。存款的过程如下:客户端发起存款请求(阶段①),agent向bank转达请求(阶段②),bank会返回操作的结果(阶段③)。代码2-16展示了一种有Bug的写法。
图2-37 代码2-16的示意图
代码2-16 agent的消息处理方法(有Bug)
local coin = 20 --角色身上的金币数 function CMD.deposit(source) if coin < 20 then --假设每次存20金币 return end local isok = skynet.call(bank, "lua", "deposit", 20); if isok then coin = coin - 20 end end
存在这么一种可能,玩家快速地两次点击存款按钮,消息时序会按图2-37中①①②③的顺序执行。如果角色身上仅剩20金币,第一次操作时,尚剩余20金币,第二次操作时,依然剩余20金币,两次操作都能成功,玩家总共存入40金币,剩余“-20”金币,显然不合理。
因为角色身上只有20金币,正常的情况是,无论玩家多么快速地点击存款按钮,他都只能成功存入一次。代码2-17展示了一种解决方法,在阻塞方法skynet.call之前扣除金币,如果存款失败,才补上扣除的金币。
代码2-17 修复代码2-16的程序
function CMD.deposit(source) if coin < 20 then --假设每次存20金币 return end coin = coin - 20 local isok = skynet.call(bank, "lua", "deposit", 20); if not isok then coin = coin + 20 end end
现在,你已掌握了Skynet的特性和基本操作,接下来的一章,会用综合示例说明怎样用Skynet去开发真正的游戏项目。这里先打个预防针,下一章的难度颇高,代码多、流程复杂,但如能掌握,你就拥有胜任“服务端开发工程师”岗位的条件。