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 :