之前对 skynet 的印象,主要是来自于我对 golang 的理解,对 gevent 开发的经验,以及云风的 blog。对于底层的代码,并没有仔细去阅读过。最近在实现业务系统的时候,发现有同事在同一个函数里做了一个互斥的判断,才发现对 skynet 的理解有误。

以前,用 python 的 gevent 框架实现游戏服务器的时候,会针对每个玩家建立 3-5 个 greenlet(协程),用于处理玩家身上的定时器事件,以及 IO 操作。然后还有少数几个协程处理全局的定时器事件。当然,战斗是放在独立的进程中实现的,那边基本上每个怪都有 1-2 个 greenlet,一个房间里有几十个 greenlet 是很正常的。总结起来,就是 python+gevent 为了规避全局锁,采用了多进程,每个进程多 greenlet 的实现方式。

而对于 golang,会变成底层是多线程模型,上面跑着多个 goroutine,当一个 goroutine 发起阻塞式 IO 的时候,底层负责跑这个 goroutine 的线程,可以随之阻塞等待。系统便随即开一个新的线程,从一堆 goroutine 里挑一个可以执行的,继续执行。golang 就相当于用多线程,取代了以前 py 的多进程。

读完云风的 blog,我对 skynet 的理解,就是每个线程来跑一个 Lua 的 VM,一系列的 VM 组成了一个队列。然后每个 VM 有自己的消息队列,VM 运行时,就从自己的消息队列里,拿出一条消息执行。得益于 Lua 的沙盒机制,即使某个 VM 发生 traceback,也能得到有效隔离。同时,因为同处一个进程内,VM 之间数据交互可以做到相当高效,字符串之类的传递只要传指针即可。

但是,我之前忽略了 Lua 协程在其中发挥的作用。原来,一个服务(即一个 VM)收到一个包 req,就会新建一个协程进行处理。处理过程中,如果产生一个对外的 call 请求,req 包是当成处理完的,当前的协程是会刮起,等待 call 请求返回来唤醒。这时候服务可以继续建立新的协程来处理新的消息包,而不会阻塞住。也就是说,如果处理一个包的过程中,发生了 skynet.call 调用,是会造成多个协程并发执行的。如果不注意用锁保护协程间的共享资源,就有可能出现问题。