skynet 是一个为网络游戏服务器设计的轻量框架。但它本身并没有任何为网络游戏业务而特别设计的部分,所以尽可以把它用于其它领域。”

作为服务器,通常需要同时处理多份类似的业务。例如在网络游戏中,你需要同时 向数千个用户提供服务 ;同时运作上百个副本,计算副本中的战斗、让 NPC 通过 AI 工作起来,等等。在单核年代,我们通常在 CPU 上轮流处理这些业务,给用户造成并行的假象。而现代计算机,则可以配置多达数十个核心,如何充分利用它们并行运作数千个相互独立的业务,是设计 skynet 的初衷。

因此,skynet提供了一种 多核并发 的解决方案,充分利用了 多核 优势。

常见的 多核并发解决方案 有: 多进程 多线程 csp模型 actor模型 。接下来简单介绍和对比这四种并发模型。

并发实体 :进程

进程间通信方式 :socket,共享内存,管道,信号量,unix域等。

优点 隔离性好 ,因为每个进程都有自己独立的进程空间

缺点 统一性差 ,即数据同步比较麻烦;解决方案(消息队列zeromq解决最终一致性问题,rpc解决强一致性问题,zookeeper解决服务协调的问题)

并发实体 :线程

线程间通信方式 :消息队列,管道,锁等

优点 统一性强 ,因为线程都在同一个进程内(这里的多线程是指同一进程内的多线程)

缺点 隔离性差 ,线程间共享了很多资源,并且可以轻易 的 访问其他线程的私有空间,需要使用锁来进行控制。(锁的类型选择和粒度控制都是比较难的)

描述两个独立的并发实体通过**共享的通讯 channel(管道)**进行通信的并发模型。

Golang 借用 CSP 模型仅仅是借用了 process 和 channel 这两个概念来实现自己的并发模型, process 是在 go 语言上的表现就是 goroutine ,也是 go 并发执行的实体,每个实体之间是通过 channel 通讯来实现数据共享。(可理解为 加强版多线程解决方案

并发实体 当然是 actor 。那么 actor 是什么呢?其实 actor 是从 语言层面 抽象出来的 进程概念 , erlang 是从 语言层面 来实现 actor 模型。(可理解为 加强版多进程解决方案

actor 模型有以下特点:

  • 用于 并行计算
  • actor 是 最基本 的计算单元
  • 基于 消息 计算
  • actor 之间 相互隔离 ,通过 消息 进行沟通

那么 skynet 也采用了 actor r模型,不过,不同于 erlang , skynet 是通过 框架 来实现 actor 模型。 skynet 使用 内存块 lua虚拟机 来进行 环境隔离 , actor 之间通过 消息队列 进行沟通,通过 指针传递 即可达到通信目的。

那么 actor 模型有哪些优势呢?我们可以启动 上千万个actor并发实体 ,而进程/线程模型中并发实体个数是 有限 的。

其实 actor 就是 skynet 中的服务,服务分为 c服务****和lua服务 (比如, main.lua 就是一个 actor ), actor 的结构组成如下:

  • 隔离环境 ,内存块或lua虚拟机
  • 回调函数 ,用于执行 actor ,消费消息
  • 消息队列 ,用于存储消息

对于 c服务 隔离环境为 内存块 lua服务 隔离环境为 lua虚拟机

 // service_logger.c 
// c服务隔离环境为内存块
struct logger {
 FILE * handle;
 char * filename;
 uint32_t starttime;
 int close;
};

// service_snlua.c 
// lua服务隔离环境为lua虚拟机
struct snlua {
 lua_State * L;
 struct skynet_context * ctx;
 size_t mem;
 size_t mem_report;
 size_t mem_limit;
 lua_State * activeL;
 volatile int trap;
};

// skynet_server.c
// context上下文隔离环境
struct skynet_context {
 void * instance;
 struct skynet_module * mod;
 void * cb_ud;
 skynet_cb cb;
 struct message_queue *queue;
  ...
};
  

lua 一般用来做业务开发( lua服务 ),c一般实现 底层框架 以及一些 计算密集型 的业务( c服务 )。**可以将skynet理解为一个简单的操作系统,可以用来调度数千个lua虚拟机(进程),让他们并行工作。**每个lua虚拟机都可以接收其他虚拟机发送过来的消息,以及对其他虚拟机发送消息。

skynet 中 actor 的运行和通信都通过消息来驱动:

  • 全局消息队列 :存储有消息的 actor 消息队列指针
  • actor消息队列 :存储 专属actor 的消息队列

如下图:

skynet_msg

工作流程:

  • 从全局消息队列中 取出actor消息队列 ,(这一步需要 加锁 ,采用自旋锁,尽可能不让worker线程休眠, 榨干cpu
  • actor消息队列 取出消息 ,并通过 回调函数 处理(消费actor中的消息);因此不用担心一个服务同时被多个线程处理,即单个服务的执行,不存在并发,也即 线程安全
  • 如果 actor消息队列 还有消息,将 actor消息队列 放入 全局消息队列的队尾 ,起到 公平调度

消息生产方式主要为

  • actor之间通信产生;
  • 网络中产生
  • 定时器产生

消息的消费方式只有一种: ,通过 回调函数 进行消费。

因为 actor 之间 通信 直接通过 指针传递 ,因此服务间的通信非常 高效

注意: actor 之间发送消息是不需要唤醒 worker 条件变量的,因为 actor 之间发送消息,则至少有一个 worker 线程在工作。

skynet 每个服务均有一个 协程池 lua服务 收到消息时,会优先去池子里取一个协程出来,即,就视为收到一个消息,就创建一个协程吧

  • timer线程 :运行定时器
  • socket线程 ,进行网络数据的收发
  • worker线程 :负责对消息队列进行调度
  • monitor线程 :用于检测节点内的消息是否堵住
 // skynet_start.c
// skynet启动是会创建以上四种线程
static void
start(int thread) {
 ...
 create_thread(&pid[0], thread_monitor, m);  // monitor线程
 create_thread(&pid[1], thread_timer, m);  // timer线程
 create_thread(&pid[2], thread_socket, m);  // socket线程
 // 根据权重创建worker线程
 static int weight[] = { 
  -1, -1, -1, -1, 0, 0, 0, 0,
  1, 1, 1, 1, 1, 1, 1, 1, 
  2, 2, 2, 2, 2, 2, 2, 2, 
  3, 3, 3, 3, 3, 3, 3, 3, };
 struct worker_parm wp[thread];
 for (i=0;i<thread;i++) {
  wp[i].m = m;
  wp[i].id = i;
  if (i < sizeof(weight)/sizeof(weight[0])) {
   wp[i].weight= weight[i];
  } else {
   wp[i].weight = 0;
  }
  create_thread(&pid[i+3], thread_worker, ℘[i]);
 }
}
  

线程间使用 管道 进行通信。其中 socket线程 worker线程 通过 pipe 进行通信。

服务模块将数据,通过 socket 发送给客户端时, 并非 将数据写入消息队列,通过 pipe worker线程 发送给 socket线程 ,并交由 socket转发

skynet 作为游戏服务器时,我们编写的不同的业务逻辑,独立运行在不同的上下文环境,并且能通过某种方式,相互协作,共同服务于玩家。

skynet 业务是由 lua 来开发,与底层沟通以及计算密集的都需要用c。

skynet 向 epoll 进行注册: connected , clients , listened , pipe读端 worker 线程往管道写端写数据, socket 线程在管道读端读数据)

skynet 中内存分配采用 jemalloc 。

以上,是为一个初学者对skynet的理解。

skynet Wiki

云风BLOG

skynet源码欣赏

Golang CSP