大家应该都了解传统的并发编程模式,多线程编程。

ShreadMemory
newthread共享内存

有并发的地方就有竞争,传统多线程的并发模式使用locks(锁),condition variable(条件变量)等同步原语来强制规定了进程的推进顺序,而这些同步原语本质上都是使用了在各个线程都可见的锁来实现,有一种全局变量的味道。(少数由硬件指令直接支持的除外,例如atomic_int。

threadshared memory

答案是有的~

今天我要分享的CSP和Actor模型都是基于消息传递的。(Message Passing)

CSP

Communicating Sequential Processes (CSP)顺序通信进程

CSP的核心思想是多个线程之间通过Channel来通信(对应到golang中的chan结构),有点像是管道的概念。(Pipe)

Actor

cat
object行为接口
cat.Move()
message

无论是CSP还是Actor模型,他们都完完全全贯彻了一句至理名言:

CSP :Goroutine

thread
chan
go

关于goroutine,还是想多聊一聊golang是如何调度goroutine的。

Go的调度器内部有三个重要的结构:M,P,S。

M:代表真正的内核OS线程,真正干活的人

G:代表一个goroutine,它有自己的栈,指令集信息(要执行的指令)和其他信息(正在等待的channel等等),调度的单位。

P:代表调度的上下文,可以把它看做一个局部的调度器,使go代码在某个M上跑

当go运行起来的时候,大致是下图的样子:

简单解释一下:

图中的两个M表示当前go运行的机器上有两个线程可以让我们使用。

P是goland的调度上下文,它必须运行在某个M上,实际上就类似:

new Thread(P.run())

P上挂着好几个goroutine :

蓝色的G表示当前P正在调度的goroutine,此时相当于把蓝色的G扔到了M上。

灰色的G表示正在等待P调度的goroutine,轮到他们的时候就会被扔到M上。(如何公平的调度灰色的G?)

目前来看,P好像是多余的,我直接想办法把G扔到M上就好了,为何还要有一个P这样的中间层的。

当然P很有必要,如果某个线程M被阻塞了,那么P可以被扔到其他的线程上,去合理的调度它挂着的G。

这个图表示的就是M0被阻塞了,转而把P挂到了M1上。

可以看到M0此时有一个G0在运行,这是啥意思…

我们在具体一点吧:

read

golang为了充分利用cpu资源,不让那些被排队的goroutine浪费时间,P挂到了M1上。

readn
G0

所有的P会周期性的检查全局队列里的G,否则这些G就都饿死了。

由此可见,P是很有必要的。
还有很神奇的一点,P可以“偷“任务。
考虑一种情况:

P1身上的G不巧都是和网络IO有关,运行的

P2身上的G都是一些比较快的运算函数,他很快就完成了,那么他就会尝试从P1那里偷一些G来运行。

channel的结构

这里可能有点抽象,上一个图更直观一点:

ch := make(chan int , 10 ) ,对应的runtime中的实现是:

malloc
mallocgc

channel的读写

这是我们在go中向channel写入数据的结构的方法,对应到:

chanval

0.各种参数的有效性的校验
1.获取channel上的锁,如果是已经关闭的channel,会释放锁否则继续逻辑:

sudogep

3.如果没有等待该channel的goroutine,看一下channel的剩余缓存是不是够大,如果可以,把数据放进去

4.缓存不够大,channel没有办法在塞数据了, 自身会因此阻塞,此时调度器会执行别的goroutine
在让出线程之前,会new一个goroutine出来,把多的数据放到这个goroutine上,加入到等待队列。

打包数据,发送等待队列

这样可能太抽象了,我们举个最简单的生产者消费者例子:

main的goroutine是生产者,以G1代替

consuemr的goroutine是消费者,以G2代替

1.初始化chan数据,创建hchan结构体。

hchan.buf指向一个大小为4的数组,并且hchan.sendx、hchan.recvx置0,hchan.dataqsiz置4。

2.发送数据

G1向channel发送数据,把要发送的数据拷贝到buf里,hchan.sendx++。(该过程需要加锁)

3.接受数据

G2从channel接受数据,将接受的数据拷贝到buf里,hchan.recvx++。(该过程需要加锁).

4.特殊情况

G1向chan发数据,chan的buf满了,会怎么样?

G1和要发的数据打包成sudogchan的sendqgoparkunlock()
chan的sendq

Actor

Actor模型在web后端应用的不是很广泛,只有一些比较小众的语言天然支持这种编程范式(erlang),但是在游戏后端算是非常常用的编程范式。

Actor模型写起来比较舒服,因为你无需担心多线程的问题,只要是Actor自身的状态是不会被外界直接改变,都是通过handlemsg的方式来改变。

大部分语言都不原生支持Actor模型,这里以C系的语言为例,给大家分享如何来实现Actor模型。

0.ActorBase -> Actor的基类

每一个线程中有一个Actor,充分利用CPU资源。

1.GlobalActorController -> 全局的Actor调度器

这个代码部分可能有些抽象,让我们举个例子:

游戏中常见的商城功能,我们定义一个

然后所有的玩家都在某个具体的场景中,我们定义一个

GlobalActorControlleractor_list

那么如果玩家想从商城里买东西应该是怎样的呢:

1.player.sendmsg(ShopActorId, buy_msg)

2.场景actor就会把这个buy_msg交付给GlobalActorController, 调用sendmsg(SceneActorId, ShopActorId, buy_msg)转发给ShopMallActor

3.商城Actor在自己的run方法里取出msg,发现了buy_msg,去handle(buy_msg), 在条件检测完毕后,发回msg给SceneActor

SceneActor通过某种策略找到到底哪个玩家发送的这个buy_msg(比如发buy_msg的时候在msg中带着player的uid),将物品发还给玩家

————————————————


LinuxC/C++、后端、服务器开发/高级架构师 面试题、学习资料、教学视频和学习路线图(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享有需要的可以自行添加学习交流群:739729163免费领取