skynet中actor模型

skynet是一个轻量级游戏服务器框架,但是不仅仅用于游戏,其他领域也可以使用这个框架。它的轻量级体现在:

  • 仅实现 actor 模型,以及相关的脚手架。sknynet目录下lualib和lualib-src都是脚手架。
  • 实现了服务器框架的基础组件。实现了 reactor 并发网络库,并提供了大量连接的接入方案;基于自身网络库,实现了常用的数据库驱动(异步连接方案),并融合了 lua 数据结构。

编译安装

先安装相应依赖。

apt-get install git build-essential readline-dev autoconf

或者

apt-get install git build-essential libreadline-dev autoconf

下载源码并编译安装

git clone https://github.com/cloudwu/skynet.git
cd skynet
make linux

多核并发编程

skynet属于多核并发编程。
多核并发编程主要有多进程、多线程(一般是一个进程里面开的多个线程)、协程、actor模型。前两者是通过共享内存来达到通信的目的,模块之间耦合度较高;后两者是利用通信来达到共享数据的目的。由于多进程和多线程模型存在资源竞争的问题,所以需要加锁,一般而言,这种锁首选自旋锁,避免进程或线程的切换。比如memcached,由于其数据结构是hash,多线程加锁时粒度比较小,通常也采用粒度较小的自旋锁,或是原子变量实现的自旋锁。
通过通信来共享数据,其实是一种解耦合的过程。并发实体之间可以分别开发并进行单独优化,而它们唯一的耦合在于消息。这能让我们快速地进行开发;同时也符合我们开发的思路,将一个大的问题拆分成若干个小问题。

actor模型

先给出一张总体的组成图。
在这里插入图片描述
下面来介绍skynet中的actor模型。
在这里插入图片描述

定义

actor 是skynet中最基本的计算单元,是基于消息进行计算和沟通的模型,用于并行计算。
actor由三部分组成:隔离的环境、消息队列、回调函数。隔离的环境主要通过 lua 虚拟机来实现;消息队列用来存放有序(先后到达)的消息;回调函数用来运行 actor,即从 actor 的消息队列中取出消息,并作为该回调函数的参数来运行 actor。

部分接口

这里给出sknynet中关于actor的部分接口,具体的实现通过下载的源码自行查看。

创建actor

在skynet.lua中

function skynet.newservice(name, ...)
	return skynet.call(".launcher", "lua" , "LAUNCH", "snlua", name, ...)
end

接下来需要看到launcher.lua中的command.LAUNCH函数,这里不再一步步跟,最后会到skynet_server.c中skynet_context_new函数,该函数会创建一个隔离的环境(lua 虚拟机),一个消息队列,并且需要设置回调函数。

struct skynet_context *skynet_context_new(const char * name, const char *param) {
	struct skynet_module * mod = skynet_module_query(name);

	if (mod == NULL)
		return NULL;

	void *inst = skynet_module_instance_create(mod);
	if (inst == NULL)
		return NULL;
	struct skynet_context * ctx = skynet_malloc(sizeof(*ctx));
	CHECKCALLING_INIT(ctx)

	ctx->mod = mod;
	ctx->instance = inst;
	ATOM_INIT(&ctx->ref , 2);
	ctx->cb = NULL;
	ctx->cb_ud = NULL;
	ctx->session_id = 0;
	ATOM_INIT(&ctx->logfile, (uintptr_t)NULL);

	ctx->init = false;
	ctx->endless = false;

	ctx->cpu_cost = 0;
	ctx->cpu_start = 0;
	ctx->message_count = 0;
	ctx->profile = G_NODE.profile;
	// Should set to 0 first to avoid skynet_handle_retireall get an uninitialized handle
	ctx->handle = 0;	
	ctx->handle = skynet_handle_register(ctx);
	struct message_queue * queue = ctx->queue = skynet_mq_create(ctx->handle);
	// init function maybe use ctx->handle, so it must init at last
	context_inc();

	CHECKCALLING_BEGIN(ctx)
	int r = skynet_module_instance_init(mod, inst, ctx, param);
	CHECKCALLING_END(ctx)
	if (r == 0) {
		struct skynet_context * ret = skynet_context_release(ctx);
		if (ret) {
			ctx->init = true;
		}
		skynet_globalmq_push(queue);
		if (ret) {
			skynet_error(ret, "LAUNCH %s %s", name, param ? param : "");
		}
		return ret;
	} else {
		skynet_error(ctx, "FAILED launch %s", name);
		uint32_t handle = ctx->handle;
		skynet_context_release(ctx);
		skynet_handle_retire(handle);
		struct drop_t d = { handle };
		skynet_mq_release(queue, drop_message, &d);
		return NULL;
	}
}

这里再给出创建时用到的一些重要函数的声明。

// 用于创建隔离的环境
void * skynet_module_instance_create(struct skynet_module *m);
// 用于设置回调函数
int skynet_module_instance_init(struct skynet_module *m, void * inst, struct skynet_context *ctx, const char * parm);
// 用于释放 actor 对象
void skynet_module_instance_release(struct skynet_module *m, void *inst);
// 用于处理 信号 消息
void skynet_module_instance_signal(struct skynet_module *m, void *inst, int signal);

运行actor

从skynet.c中开始

struct message_queue * skynet_context_message_dispatch(struct skynet_monitor *sm, struct message_queue *q, int weight) ;

函数中有从消息队列中取出消息操作skynet_mq_pop(q,&msg),然后执行消息dispatch_message(ctx, &msg);最后在这个函数中会执行回调函数ctx->cb,这里就不再一一跟下去了。

actor消息

actor是基于消息进行计算的模型,在 skynet 框架中,actor消息包含三种: actor之间发送的消息、网络消息以及定时消息。

actor之间消息

主要通过两种方式发送消息

-- addr 对端服务的地址
-- typename 消息类型 actor内部间通常为 lua 类型消息
-- ... 为可变参
skynet.send(addr, typename, ...)
-- addr 对端服务的地址
-- typename 消息类型 actor内部间通常为 lua 类型消息
-- ... 为可变参
-- 注意:
-- 对端需要显示调用 skynet.ret(...)或skynet.retpack(...) 回应 skynet.call 的请求,两者区别在于是否自动打包
-- 或者通过调用 skynet.response() 延迟回应 skynet.call 的请求
skynet.call(addr, typename, ...)

网络消息

skynet 当中采用一个 socket 线程来处理网络信息。skynet 基于 reactor 网络模型,在linux中使用epoll。网络当中获取数据,怎么知道传递到哪个服务的消息队列当中去?这就需要做到fd与相应的actor绑定。
skynet通过 epoll_ctl 设置 struct epoll_event 中设置data.ptr = (struct socket *)ud;来完成fd 与 actor绑定,通过 socket.start(fd, func) 来完成 actor 与 fd 的绑定。

定时消息

skynet 采用多层级时间轮来解决多线程环境下定时任务的管理,时间复杂度为 O ( 1 ) O(1) O(1),当定时任务被触发,将会向目标 actor 发送定时消息,从而驱动 Actor 的运行。

actor调度

工作线程从全局队列中 pop 出单个 actor 消息队列,从 actor 消息队列中按照规则 pop 出一定数量的消息进行执行。执行这个数量的消息后,若 actor 消息队列中仍有消息,则将该actor放入全局队列队尾;若 actor 消息队列中没有消息则不放入全局队列中。这也确保了全局队列只存活跃的 actor 消息队列。
在这里插入图片描述

工作线程执行规则

工作线程数量是按照 cpu 核心数来设置的,一般设置的线程数等于cpu核心数。工作线程按照下面工作线程权重图来设置每个工作线程的权重。

// 工作线程权重图
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, };

权重<0,worker线程一次消费一条消息(从actor消息队列中pop一个消息);权重=0,worker线程一次消费actor消息队列里所有的消息;权重>0,假设actor消息队列的长度为 l e n len len,worker线程一次消费actor消息队列里 l e n / 2 w e i g h t len/2^{weight} len/2weight条消息。
由于前4个线程一次只执行一条消息,这就确保没有消息会处于饥饿状态。

int i,n=1;
for (i=0; i<n; i++) {
// 注意: skynet_mq_pop pop出消息则返回0,没有pop消息返回1
    if (skynet_mq_pop(q, &msg)) {
        skynet_context_release(ctx);
        return skynet_globalmq_pop();
    } else if (i==0 && weight >= 0) {
        n = skynet_mq_length(q);
        n >>= weight;
    }
...
    // 调用 actor 回调函数消费消息
    dispatch_message(ctx, &msg);
}

通过这种方式,完成消息队列梯度消费,从而不至于让某些队列过长。这种消息调度的方式不是最优的调度方式(相较于 go 语言)。云风也在尝试修改更优的方式来调度,但是目前从多年线上实践情况来看,skynet 运行良好。

actor锁问题

关于actot的加锁,有以下几点:

  1. 在skynet工作过程中,会有多个工作线程从全局消息队列中取次级消息队列的情况,应该采用什么锁?
    从两个方面考虑,一是skynet 目标是为了充分利用多核,尽量减少 cpu 切换;二是队列操作粒度很小,全局队列的 pop 和 push 操作只是少量的运算。所以,考虑使用自旋锁,互斥锁会引起核心切换。
  2. 当 skynet 全局消息队列节点很少的时候,怎么让多余的工作线程得到休眠?如果此时全局消息队列节点很多后,怎么让休眠的工作线程得到唤醒?
    使用条件变量+互斥锁。对于调度,可以使用条件变量,也可以使用信号量。但是,因为信号量是固定数量的,而skynet中actor数量是不固定的,有时多有时少,所以无法使用需要准确数量的信号量。

总结一下:

  1. actor 内部若涉及多线程应考虑加自旋锁或原子操作,避免在工作线程执行过程中被切换;
  2. actor 内部若涉及多线程应考虑临界区域操作不能过于耗时,避免长期占用工作线程让同消息队列中其他消息得不到及时执行;
  3. actor 单个消息业务应避免阻塞线程(注意不是协程)的操作。如果这个操作必不可少,另起一个外部进程,skynet 进程用 socket 与之通信,这种阻塞或者耗时操作的任务交由外部服务来处理。当然,也可以在skynet添加一个线程,参考timer线程的实现。

最后再补充一下,锁中还有一种较为重要的锁是读写锁。读写锁适用于读多写少的情况。此外,对于读多写少的情况,有些框架还可以不用加读写锁,如mvcc和read-on-copy。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值