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 handlectx->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 lastcontext_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消息队列的长度为lenlenlen,worker线程一次消费actor消息队列里len/2weightlen/2^{weight}len/2weight条消息。
由于前4个线程一次只执行一条消息,这就确保没有消息会处于饥饿状态。

int i,n=1;
for (i=0; i<n; i++) {// 注意: skynet_mq_pop pop出消息则返回0,没有pop消息返回1if (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。

skynet中actor模型相关推荐

  1. skynet源码分析:服务,Actor模型,lua接口编程,demo演示Actor编程思维

    skynet刚开始是单进程多线程的,它是由一个一个的服务组成的.在skynet上做开发,实际上就是在写服务.服务与服务之间通过消息队列进行通信. 做为核心功能,Skynet 仅解决一个问题: 把一个符 ...

  2. 【云风skynet】详解skynet的多核高并发编程丨actor模型丨游戏开发丨游戏服务端开发丨多线程丨Linux服务器开发丨后端开发

    skynet中多核高并发编程给我们的启发 1. 多核并发编程 2. actor模型详解 3. 手撕一个万人同时在线游戏 视频讲解如下,点击观看: [云风skynet]详解skynet的多核高并发编程丨 ...

  3. 在.NET中实现Actor模型的不同方式

    上周,<实现领域驱动设计>(Implementing Domain-Driven Design)一书的作者Vaughn Vernon,发布了Dotsero,这是一个使用C#编写的.基于.N ...

  4. 云风skynet-如何用skynet手撕万人同时在线游戏丨actor模型详解

    如何用skynet手撕万人同时在线游戏 1. 多核并发编程 2. actor模型详解 3. 游戏实现 视频讲解如下,点击观看: 云风skynet-如何用skynet手撕万人同时在线游戏丨actor模型 ...

  5. 游戏服务器为什么选用Actor模型,《百万在线》给你答案

    游戏服务器需要承载很多玩家,开发者需要选择合理的架构.Actor模型是一种常用的游戏服务端架构模型,在选用Skynet.Erlang方案的公司中广泛应用. 从硬件角度看待架构划分 游戏服务端选用的抽象 ...

  6. java actor模型实例,详解Theron通过Actor模型解决C++并发编程的一种思维

    现今,单台机器拥有多个独立的计算单元已经太常见了,这点在服务器的处理器上表现尤为明显,据AMD的一张2012-2013服务器路线图显示,服务器处理器的核心数将在2013年达到20颗之多,合理的利用CP ...

  7. Akka入门(二)Akka的Actor模型如何满足现代分布式系统需求

    Actor模型允许开发者: 在不诉诸锁定的情况下实施封装. 使用协作实体的模型对信号做出反应,改变状态,并相互发送信号以推动整个应用程序向前发展. 不要担心与我们的世界观不匹配的执行机制. (一) 消 ...

  8. Actor模型(分布式编程)

    Actor的目的是为了解决分布式编程中的一系列问题.所有消息都是异步交付的,因此将消息发送方与接收方分开,正是由于这种分离,导致actor系统具有内在的并发性:可以不受限制地并行执行任何拥有输入消息的 ...

  9. 《通过C#学Proto.Actor模型》之 HelloWorld

    在微服务中,数据最终一致性的一个解决方案是通过有状态的Actor模型来达到,那什么是Actor模型呢? Actor是并行的计算模型,包含状态,行为,并且包含一个邮箱,来异步处理消息. 关于Actor的 ...

最新文章

  1. 新科技快速指南系列之“5G”:历史、现在与未来
  2. real210移植记录-支持eMMC,增加菜单操作
  3. GitHub Copilot 现已登陆 Visual Studio!
  4. PDOStatement::bindParam的一个陷阱
  5. 使用vmstat和iostat命令进行Linux性能监控
  6. Hibernate中封装session(静态单例模式)
  7. 判断包含字符当中包含小数点_Python|提取包含指定文本的行到一个文本文件(字符串或正则)...
  8. 为什么C预处理程序将单词“ linux”解释为常量“ 1”?
  9. CakePHP 3.7.6 发布,PHP 快速开发框架
  10. C#+Halcon调用Basler相机
  11. 【性能监测】前端性能监测方法总结(非监测平台)
  12. 《最强大脑》之四色定理—— GIS 无处不在
  13. 【MySQL(七)】脏页
  14. DBCO-PALA-b-PEG DBCO-聚硫辛酸-聚乙二醇
  15. WEB电商项目广告管理与缓存解决方案
  16. 上帝视角学JAVA- 基础08-类06【2021-08-07】
  17. Unknown Faceted Project Problem (Java Version Mismatch)
  18. 企业上云是什么?有什么优点
  19. Python 之 进阶 学习
  20. 换发型算法_今年烫发就选“大C卷烫发”,想换发型的女生可以做参考!

热门文章

  1. 第十一章:偏最小二乘回归分析
  2. 哪些公司需要做环境管理体系认证?
  3. 社畜跑路、校招上岸、润
  4. android危险权限分组,Android 中的危险权限详细整理
  5. 信息论通识教育课程:“幸存者偏差”(survivorship bias)的应用和数学基础
  6. 1.1 操作系统的目标和作用
  7. 文墨绘学恋爱也是孩子的学习课程
  8. 妙!二十九招驱蚊止痒不再愁
  9. Jenkins定时构建时间设置
  10. 数据库分析与设计练习题(二)基础知识、在IDEF0需求建模方法中,下列关于描述功能活动的图示,正确的是、事务详细编码设计是在下列哪个阶段完成的、下列关于从属实体集的描述,错误的是、各事务尽量按照相同的