首先贴下多进程单线程和单进程多线程的特点:
多进程:有独立的地址空间,进程之间不共享内存和变量,但可以通过共享内存实现,每个进程只有一个线程,一般用于单机系统开发。
多线程:在同一个进程下的所有线程可以共享内存和变量。
而共同点是,同开辟的进程数/线程数多于系统cpu核数时,无法继续提高应用的性能。
而多线程架构的服务器,只要适当将一些任务分出来用新的进程启动,就可以扩展成分布式架构,使用tcp通信即可。当然多进程也可以这么干,通信方式也是使用tcp。
而操作系统对于线程的切换是比进程的切换要快。
下面先介绍下多进程单线程服务器架构,以单机系统为例:
先贴架构图:
一个游戏服大概就有这几个进程。
router:作用如其名,路由。 每个功能进程启动时,会先连接router,router会给连上来的进程分配一个唯一标识,所有功能进程都是靠这个router进程通信。
login: 登录服务器,client登录验证在这个进程进行。
logic:玩家单人逻辑操作处理进程,login会将登录的玩家平摊到这些logic上。
global_logic: 全局操作进程,多人玩法的功能,例如战斗匹配,工会等操作会放在这里进行。
log:游戏日志输出进程,所有功能进程的日志输出都先发到这个进程,log进程再输出到磁盘文件,
db: redis作为内存数据库,mysql作为数据持久化,其他功能进程取数据都会发送请求到db。
back: 后台进程,集成了一个http服务器,可以处理http请求,这里可以集成一些第三方服务功能,如gm指令。
以上每个进程都是单线程,所以无需考虑锁的问题。
对于每个进程收发数据:
发数据:直接把 {target_id: data} 发送到router,
收数据:帧驱动,如100ms主动向router询问是否有数据,有则取过来进行处理。
单机系统下,如果采用共享内存方式,通信效率将非常高。
所以多进程的服务器架构设计起来还是比较简单的。
再介绍下多线程服务器的架构,这里我想介绍actor模型。
一个Actor指的是一个最基本的计算单元。它能接收一个消息并且基于其执行计算。
这个理念很像面向对象语言,一个对象接收一条消息(方法调用),然后根据接收的消息做事(调用了哪个方法)。
Actors一大重要特征在于actors之间相互隔离,它们并不互相共享内存。这点区别于上述的对象。也就是说,一个actor能维持一个私有的状态,并且这个状态不可能被另一个
actor所改变。
每个Actor都有一个邮箱,用于接收其他actor发送的消息。
这里重点要讲下Actor模型的调度是怎样做的。
Actor模型实际上可以有成千上万个,但目前一台通用服务器最多只有24核,当然不可能也开成千上万个线程。
我们可以把Actor简单想象成这样一个类实例:
class Actor { public: void process_1(); void process_2();
void fetch_msg(); private: int actor_id; string actor_name;
list<msg> msg_queue; }
每个Actor定义了自己实现的功能(process_1, process_2).
当msg_queue邮箱有消息到来的时候,就调用fetch_msg去获取这些消息进行处理。
这一步就是靠调度线程来做了。
Actor模型的调度实现起码要有:
1. 一个位于主线程的Actor队列,如 global_queue<Actor*> gq, 当某个Actor收到消息时,就会被放进这个gq,等待工作线程进行调度。
2. n个工作线程,这个就要根据机器的核数来决定开多少个了,例如只是一台双核的机器,那么开一个就好了,开多了就会浪费时间在线程切换上,得不偿失。
每个工作线程做的事件很简单,向主线程询问任务,获取任务,处理任务,然后又继续询问,大致如下:
while(true) { task_list = fetch_task(); process_task(task_list); }
所以一个Actor的创建和调度过程如下:
1. 在主线程创建并放入管理列表.
2. 其他actor往本actor发送消息,消息进入msg_queue,本actor进入 global_queue等待调度。
3. 有工作线程处理完一堆任务了,向主线程询问任务,主线程把本actor分配给这个工作线程。
4. 该工作线程取出msg,调用actor相应处理函数处理这个消息。
所以可见,actor数目与工作线程数目没有必然的关系,当然理想状态是,每个actor都有自己的处理线程,这里有消息来到时,就可以马上处理,不用等待。
理论上,actor开的越多,业务逻辑就分的越细,每次处理的时间就越短,只要actor的数目超过线程数,就可以最大限度利用多核的优势,cpu的调度就越充分。
所以actor模型设计关键在于如何将业务逻辑平摊到更多的actor上,而不是集中。 例如上面提到global_logic是多人玩法的业务逻辑,只要一细分,可以把分成
帮会actor,组队actor,战斗actor等等,这样三个消息同时就有机会被三个cpu处理,而不是固定只有一个。
Actor可以理解成用户级别的进程,与操作系统级别的进程分离,即使开很多Actor,只要工作线程数目设计合理( <= 系统cpu核数),就能保证线程能在一直同一个cpu上
进行操作,减少线程切换的消耗,这对于cpu核数小的机器非常有用。 而对于像24核的机器,因为开辟的线程数是配置的,所以也很好规划一台机器能部署多少个服。
而多进程如果要对某些功能进行扩展,如增加login,增加logic,就是要增加一个系统线程,一旦进程数超过了cpu核,就会有时间浪费在切换线程上了,
这是一个缺点。
而Actor模型本身是优秀的,但是Actor的调度算法有会有很多种实现,而且必然涉及到锁的设计,这就需要设计者的设计功力了。