skynet设计原理


前言:框架学习思路
1、掌握框架是怎么解决问题的 2、掌握框架的核心开发技能 3、掌握框架的开发思路
目的是基于框架做正确的事情

一、多核并发模型

erlang是从语言层面上解决actor并发模型
skynet从框架层面去实现actor并发模型
go语言从语言层面上去实现csp并发模型

多线程

在一个进程中开启多线程,为了充分利用多核,一般设置工作线程的个数为 cpu 的核心数;
memcached 就是采用这种方式;
多线程在一个进程当中,所以数据共享来自进程当中的内存;这里会涉及到很多临界资源的访问,
所以需要考虑加锁;

多进程

在一台机器当中,开启多个进程充分利用多核,一般设置工作进程的个数为 cpu 的核心数;
nginx 就是采用这种方式; ngx_shmtx_t自旋锁信号量文件锁nginx 当中的worker进程,通过共享内存来进行共享数据;也需要考虑使用锁;

CSP

以 go 语言为代表,并发实体是协程(用户态线程、轻量级线程);内部也是采用多少个核心开启多少个内核线程来充分利用多核;

Actor

actor其实就是抽象的进程
erlang 从语言层面支持 actor 并发模型,并发实体是actor(在 skynet 中称之为服务);skynet采用 c + lua来实现 actor 并发模型;底层也是通过采用多少个核心开启多少个内核线程来充分利用多核;

总结

不要通过共享内存来通信,而应该通过通信来共享内存;
CSP 和 Actor 都符合这一哲学;
通过通信来共享数据,其实是一种解耦合的过程;并发实体之间可以分别开发并进行单独优化,而它们唯一的耦合在于消息;这能让我们快速地进行开发;同时也符合我们开发的思路,将一个大的问题拆分成若干个小问题;

二、skynet

它是一个轻量级游戏服务器框架,而不仅仅用于游戏;
轻量级体现在:

  1. 实现了 actor 模型,以及相关的脚手架(工具集);
    actor 间数据共享机制;c 服务扩展机制;
  2. 实现了服务器框架的基础组件;
    实现了 reactor 并发网络库;并提供了大量连接的接入方案;
    基于自身网络库,实现了常用的数据库驱动(异步连接方案),并融合了 lua 数据结构;
    实现了网关服务;
    时间轮用于处理定时消息;

环境准备

centos
yum install -y git gcc readline-devel autoconf
ubuntu
apt-get install git build-essential readline-dev autoconf
#或者
apt-get install git build-essential libreadline-dev autoconf
mac
brew install git gcc readline autoconf
编译安装
git clone https://github.com/cloudwu/skynet.git
cd skynet
#centos or ubuntu
make linux
#mac
make macosx

这是一个游戏项目的组织结构
在这里插入图片描述我们在linux环境下去编译一下,就能生成skynet的一个可执行文件
skynet与redis一样,在启动的时候都需要指定一个配置文件game.conf
在这里插入图片描述

game.conf

我们打开这个game.conf吧

thread=8 #表示开启8个内核线程来运行actor模型,也是线程池大小
logger=nil #在使用skynet的时候需要打印一些数据,如果是nil,就表示直接打印在控制台上,如果是具体文件名就打印在这个文件中
harbor=0#0表示不使用集群
start="main" -- 启动服务,在运行c/c++代码的时候,这相当于入口
lua_path="./skynet/lualib/?.lua;" .. "./skynet/lualib/?/init.lua"#这个问号表示任意字符,也就是任意后面带.lua的文件,跟*.lua一个意思
luaservice="./skynet/service/?.lua;" .. "./game/?.lua"#lua生成的服务
lualoader="./skynet/lualib/loader.lua"
cpath = "./skynet/cservice/?.so"
lua_cpath = "./skynet/luaclib/?.so"

main.lua

local skynet = require "skynet"
local socket = require "skynet.socket"

skynet.start(function ()--这个入口开始运行
    skynet.uniqueservice("redis") --首先启动一个redis的服务
    local listenfd = socket.listen("0.0.0.0", 8888)--实现网络编程,绑定ip与端口,一连接这个,就会执行下边的回调函数
    socket.start(listenfd, function (clientfd, addr)--fd与服务器分配的ip与端口
        skynet.newservice("agent", clientfd, addr)--为连接生成一个服务,冒号里边的字符串就是文件名,通过这个执行agent这个文件的内容,通过这个文件去创建一个服务,加载agent这个文件
    end)
end)
--轻量级游戏服务器框架,不仅仅是游戏,有的金融业务也用这个,基于actor的并发模型

redis.lua

local skynet = require "skynet"

local redis = require "skynet.db.redis"

skynet.start(function ()
    local rds = redis.connect({ --首先连接redis,访问数据库
        host = "127.0.0.1",
        port = 6379,
        db = 0, -- select db
    })
    skynet.dispatch("lua", function (_, _, cmd, ...)
        skynet.retpack( rds[cmd](rds, ...))
    end)
end)

agent.lua
红框里表示创建服务时的参数
将client与addr传进去
在这里插入图片描述接收服务,从这个入口开始
在这里插入图片描述
我们来运行一下,执行game.conf
在这里插入图片描述
我们用telnet去连接这个服务器,我们就接收到了一个客户端的连接,并且还LAUCH到一个服务
在这里插入图片描述
我们再登录一下

在这里插入图片描述
我们看到redis当中已经有这个数据了
在这里插入图片描述
我们主要在这里边开发我们的服务,我们的服务就是我们熟知的抽象的进程,这种抽象进程有一个很好的优点就是提供了一个隔离的环境,这种进程就是一个运行实体
这个就是我们服务的定义,也就是我们抽象进程的定义
第一个instance就是一个隔离的运行环境,可能是一块内存或者lua虚拟机
mod表示我们用的哪个启动文件,也就是之前提到的agent
接下来是回调函数
queue表示消息队列,按照消息队列顺序去运行
在这里插入图片描述
总之,这样的组成部分:1、隔离环境(主要指lua虚拟机) 2、回调函数 3、消息队列(actor就是通过这种消息来沟通的)
以上也就呼应了,通过通信来共享内存

三、Actor消息

Actor 模型基于消息计算,在 skynet 框架中,消息包含 Actor (之间)消息、网络消息以及定时消息;
我们的连接会通过这个main来处理连接的消息
网络消息,actor之间的消息,定时消息(skynet当中有一个单独的线程,使用的是时间轮算法,这种单独的线程来处理定时任务,把这个定时任务包装成消息,把消息传递到actor当中,actor就能去处理这种定时消息)
在这里插入图片描述新的网络消息如何传递到新的agent服务中呢
在这里插入图片描述
我们可以通过这个socket.start,把具体的fd传递到服务当中,与agent的抽象的服务进行一个绑定,fd产生的环境都可以传递进去。意思就是说我们的skynet的任务都以消息的方式进行传递。
在这里插入图片描述

以上的内容啊,总结起来就是,服务就是抽象的进程,我们需要一个隔离的环境(lua虚拟机),我们通过回调函数去运行,运行的依据就是消息队列当中的消息,这个消息作为参数去调这个回调函数,回调函数一运行这个actor就运行了。一个服务就是一个actor

reactor网络模型的主要是,异步和事件
我们需要考虑事件怎么和actor进行绑定
在这里插入图片描述

四、线程池的原理

生产者线程,消费者线程,把任务理解成一种消息,任务携带上下文和函数,根据上下文传递到其他线程去。线程池就是我们的消费者,消费者线程是为了取出任务执行任务在这里插入图片描述

skynet当中的线程池

红圈里边的就是我们的线程池,
圆的就是我们的抽象的进程,每个抽象的进程当中都有一个队列,底下方框表示的我们的全局队列,生产者有我们的socket-thread,还有我们的定时线程timer-thread产生消息
socket-thread当中有事件循环,事件循环当中我们会取出具体的事件(图中紫色部分),把事件包装成一个消息,发送到具体的actor当中去(就是图中的圆圈),socket-thread是生产者。actor会给timer-thread一个任务,时间到了以后包装一个消息,会返回给actor当中去。线程池驱动actor运行的,消息的产生是由work thread产生的(actor与actor产生消息的时候),所以work线程也是个生产者。
在这里插入图片描述
在这里插入图片描述
我们来看看这个调度流程是怎么样的
我们先找到work线程来看看调度
在这里插入图片描述
再看看我们的工作线程是如何初始化的,create_thread创建线程,每一个线程携带参数为wp[i],
在这里插入图片描述
我们来看看工作线程,thread_worker我们的工作线程的初始化流程是什么,工作线程的任务就是以下这些

在这里插入图片描述
里边有个死循环,163行是取出任务
在这里插入图片描述
301行就是取出一个消息队列,然后ctx就是我们的actor,获取ctx以后就开始取消息了
在这里插入图片描述
skynet_globalmq_pop()函数定义里边有一个自旋锁,队列当中的元素pop出来我们会获得一个message
在这里插入图片描述

为啥要用自旋锁而不选互斥锁,比如说两个核心,三个线程
比如核心1执行a线程,也就是a拿到锁了,核心2运行b线程,b也尝试获取锁
如果是互斥锁,b线程发现a获取了锁,那么b会挂起。核心2会发生切换去运行c,互斥锁会发生线程切换
自旋锁不会让出执行权,空转cpu的方式检测锁是否被释放
不使用mutex是因为锁的力度太小,我们操作的是消息,队列存取比较简单
在这里插入图片描述
画圈的地方就是执行消息,将消息进行分发
在这里插入图片描述
我们来看看队列调度,队列当中没有任务就让他休眠,在画圈的地方进入休眠的流程
在这里插入图片描述
这两个临界资源的操作都是在互斥锁的保护下进行的,这个就是线程的调度方式
在这里插入图片描述
网络线程与定时线程产生消息的时候,肯定要唤醒。actor之间的消息要不要wakeup,actor与actor之间产生消息,q队列不可能为空,所以不会产生wakeup。actor的调度应该在哪个流程中呢
我们这里有个全局队列的pop,我们每个actor有个专属队列,我们有个全局消息队列,也就是线程池那个图里的下方当中的方块队列,那么里边组织的又是什么数据呢?
skynet有千千万万个抽象的进程,去调度有消息的消息队列,也就是有活跃的actor
在这里插入图片描述
怎么pop出一个活跃的消息队列呢,我们可以看到里边用的自旋锁
在这里插入图片描述
线程的权重,权重是什么意思,前四个是-1,后四个是0,只有权重>=0的时候才会走修改n的值,前四个线程不会修改n,后4个线程可以修改n,n为消息队列的长度,每次消费消息队列所有的消息
在这里插入图片描述

以上的整个流程就是,actor是抽象的进程,需要隔离的环境(lua虚拟机),也就是多个服务单独的进行,每个进程运行时不会影响其他进程的数据。从main函数出发运行到return,actor要运行就要指定一个回调函数,那么怎么区分每一个actor运行时的不同的内容,我们是以消息为回调函数的参数的方式去区分。有多个消息,怎么样去存储。按照消息到达先后顺序用一个消息队列去存储消息。我们的工作线程取出消息队列的消息做为回调函数的参数。通过这些步骤来运行actor。我们下次就会去讨论事件如何与actor进行绑定。
那我们就要考虑怎么去调度这些消息和actor,最主要的核心就是线程池,线程池首先要分析哪些是生产者消费者。socket-thread收集网络当中的所有的事件,将消息分发给actor当中。actor需要一些延时任务,就需要一些定时线程,产生的定时消息也会被线程返回插到消息队列当中去。生产知道了,我们如何去消费呢,线程池在运行的时候呢,会首先去监测活跃的actor(就是图中底部的全局队列),把这些活跃的actor一个个取出来执行,这里的消息队列是并发执行的
使用权重的原因是错位执行,这是一种优化,当初skynet是没有错位执行的,起初发现了一种不公平的状态,如果权重一致会出现消息堆积的状态。错位为啥会解决这个问题呢,从概率上去理解,执行每个队列的概率一样,设置不同的权重会尽快去处理堆积的消息队列。

协程

便捷性就在于协程这个概念,skynet是一条消息对应一个协程,在回调函数当中会有这个协程
在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值