七叶笔记 » golang编程 » Skynet中的并发模型

Skynet中的并发模型

skynet是什么?

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

skynet设计初衷

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

并发编程模型解决方案

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

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

多进程并发模型

并发实体 :进程

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

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

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

多线程并发模型

并发实体 :线程

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

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

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

csp并发模型

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

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

actor模型

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

actor 模型有以下特点:

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

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

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

skynet中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服务 收到消息时,会优先去池子里取一个协程出来,即,就视为收到一个消息,就创建一个协程吧

skynet中的线程

  • 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

相关文章