Actor
并发actor(数据+行为+消息)之间并不共享内存,不存在相互调用,只有消息通知。分布式的特点,不在意是在本地还是远程,很好的解决了数据共享的问题(自身维护自己的数据状态,只通过接受消息进行处理请求,内部串行处理消息)
skynet服务
概述
Skynet 只负责把一个数据包从一个服务内发送出去,让同一进程内的另一个服务收到,调用对应的callback 函数处理。它保证,模块的初始化过程,每个独立的 callback 调用,都是 相互线程安全 的。编写服务的人不需要特别的为多线程环境考虑任何问题,专心处理发送给它的一个个数据包
- C语言 实现的消息循环和组件加载机制;
- lua 实现的以消息为中心的进入退出 coroutine(协程)的包装层。
//初始化节点模块,用于集群,转发远程节点的消息
skynet_harbor_init(config->harbor);
//初始化句柄模块,用于给每个Skynet服务创建一个全局唯一的句柄值
skynet_handle_init(config->harbor);
//初始化消息队列模块,这是Skynet的主要数据结构
skynet_mq_init();
//初始化服务动态库加载模块,主要用于加载符合Skynet服务模块接口的动态链接库(.so)
skynet_module_init(config->module_path);
//初始化定时器模块
skynet_timer_init();
//初始化网络模块
skynet_socket_init();
//加载日志模块
skynet_profile_enable(config->profile);
服务与消息的关联
初始化一个服务:
- 一个skynet_contex作为服务的实例
- 一个唯一的服务handle,即服务的唯一id,用于识别
- 一个消息队列message_queue(该服务的队列,即次级队列)
- 向框架注册一个callback,当服务收到发来的消息,使用这个方法
handle是32位整数,生成时把该节点的harbor id写入handle的高8位 (因为只有8位,则skynet集群最多256个节点, 一个节点中最多24位个服务,1.6M个 (低24位用来分配给本节点的handle)
消息队列
在skynet中所有的消息都是异步的
- ctx->queue:每个服务的私有消息队列
- global_queue:全局消息队列
- skynet_globalmq_push:二级消息队列压入全局消息队列的接口
- skynet_context_message_dispatch:从消息队列中取出消息来执行
在服务被创建时有自己的消息队列,并且注册到全局的消息队列中 (严格上:全局存放的是私有消息队列不为空的Actor)
struct skynet_message{
uint32_t source; //服务实例handle
int session; //上下文的标识
void* data; //消息地址指针
size_t sz; //size
};
//
//全局队列
struct message_queue{
struct spinlock lock; //锁
uint32_t handle; //消息队列所属服务的句柄
int cap; //容量
int head;
int tail;
int release; //队列是否被释放
int in_global; //是否存入全局队列标志
struct skynet_message * queue;
struct message_queue *next; //与其他消息队列的关联
}
//全局消息队列结构
struct global_queue {
struct message_queue *head;
struct message_queue *tail;
//锁(自旋锁或互斥锁)
struct spinlock lock;
};
//全局消息队列的静态结构指针
static struct global_queue *Q = NULL;
工作线程
static void *thread_worker(void *p) {
//初始化
struct worker_parm *wp = p;
int id = wp->id;
int weight = wp->weight;
struct monitor *m = wp->m;
struct skynet_monitor *sm = m->m[id];
skynet_initthread(THREAD_WORKER);
struct message_queue * q = NULL;
//循环调用 skynet_context_message_dispatch
while (!m->quit) {
q = skynet_context_message_dispatch(sm, q, weight);
//消息队列无消息可执行则挂起当前工作线程
if (q == NULL) {
if (pthread_mutex_lock(&m->mutex) == 0) {
++ m->sleep;
// "spurious wakeup" is harmless,
// because skynet_context_message_dispatch() can be call at any time.
if (!m->quit)
pthread_cond_wait(&m->cond, &m->mutex);
-- m->sleep;
if (pthread_mutex_unlock(&m->mutex)) {
fprintf(stderr, "unlock mutex error");
exit(1);
}
}
}
}
return NULL;
}
通过while 不断的从skynet_context_message_dipatch 从全局消息对列中取出消息来执行,直到全部执行完,当前线程进入wait
- 首先,通过 skynet_globalmq_pop 从全局消息队列中得到一个服务的私有消息队列 q;
- 然后,通过 skynet_mq_pop 从 q 中取到一条具体的消息来执行;
- 最后,通过 dispatch_message 执行了消息后,判断当前 q 队列是否已经没有其他消息:假如有则将 q 通过 skynet_globalmq_push 放回全局消息队列中;假如没有,则不再将 q 放回。
注册回调函数
function skynet.start(start_func)
-- 将dispatch_message 绑定为消息的回调函数
c.callback(skynet.dispatch_message)
end
RPC
向外部服务发起一个远程调用,等待对方发送回应之后逻辑再继续执行下去,为了把这种回调函数的模式转为阻塞api,引入coroutine,运行一半时挂起,之后在适当的时候继续运行。。。。所以在接收到每条消息都会创建一个co 去执行该消息的dispatch
local command = {}
function command.foobar(...)
end
local function dispatch(cmd, ...)
command[cmd](...) --第一种
--第二种更好
local f = assert(command[cmd])
--执行 cmd 对应的处理函数
local r = f(...)
--判断是否有返回结果
if r ~= NORET then
--打包数据并回传给消息发送方
skynet.ret(skynet.pack(r))
end
end