CSP和Actor两种并发模型分析
背景
在多核CPU机器下,为了充分利用多核计算机的资源,我们需要进行并发编程,提高对CPU的利用率,
# 提示:如你已了解或无意了解并行和并发,可以跳过本段,直达标题 ”传统并发和基于消息传递的并发“
并行(parallel): 物理上同一时间处理不同任务
并发(concurrent): 逻辑上处理同时的任务的能力
通常所说的并发编程,也就是说它允许多个任务同时执行,但实际上并不一定在同一时刻被执行。
在单核处理器上,通过多线程共享CPU时间片串行执行(并发非并行)。
而并行则依赖于多核处理器等物理资源,让多个任务可以实现并行执行(并发且并行)。
如下是一些编程语言,以及它们相应的并发机制:
Actors Model — Erlang, Scala, Rust
CSP — Go-lang
多线程 — Java, C#, C++
1 传统并发和基于消息传递的并发
并发从大的角度上讲,并发分为传统并发和基于消息传递的并发。
1.1 传统并发模型
多线程编程,就是传统的并发编程模式,传统的多线程编程,使用的是ShreadMemory(共享内存)的方式,来实现的。
有并发的地方就有竞争:多线程下,有可能多个线程同时访问/修改同一个变量或者文件,从而导致数据污染或程序异常。
我们平时所用的通过使用各种lock(锁),condition (条件变量)等同步原语来保证多线程对同一块内存地址(也可能是文件)的顺序访问,从而保证共享数据的正确性,来实现线程安全。实际上这些同步原语本质上也都是在各个线程中使用了锁来来实现的。
除了基于shared memory(共享内存)以外,还有什么其他的并发模型吗?
答案是有的,就是基于消息的并发模型
1.2 基于消息的并发模型
基于消息传递(Message Passing)的并发模型有两种:Actor模型和CSP模型
基于消息的并发模型本质上就是:不通过共享内存进行通信;而是通过通信进行共享内存。
1.2.1 Actor模型
Actor模型被用来解决分布式程序的问题而设计,因此它非常适合跨多台机器扩展
在Actor模型中,主角是Actor,类似一种Worker。
Actor彼此之间直接发送消息,不需要经过什么中介,消息是异步发送和处理的:
Actor模型描述了一组为了避免并发编程的常见问题的公里:
1、所有Actor状态是Actor本地的,外部无法访问;
2、Actor必须只有通过消息传递进行通信;
3、一个Actor可以响应消息:退出新Actor,改变其内部状态,或将消息发送到一个或多个其他参与者;
4、Actor可能会阻塞自己,但Actor不应该阻塞它运行的线程;
1.2.2 CSP模型
Communicating Sequential Processes 即通信顺序进程简称CSP,是依赖于一个通道channel完成两个通信实体之间协调的并发模型。它基于不共享内存的消息传递.
使用Channel后,worker之间不直接彼此联系,而是通过不同channel进行消息发布和侦听。消息的发送者和接收者之间通过Channel松耦合,发送者不知道自己消息被哪个接收者消费了,接收者也不知道是哪个发送者发送的消息。
Go语言的CSP模型是由协程Goroutine与通道Channel实现:
Go协程goroutine:
是一种轻量线程,它不是操作系统的线程,而是一个操作系统线程分段使用,通过调度器和GMP实现协作式调度。
是一种绿色线程,微线程,正常一个线程自己的栈内存是固定的2MB,而goroutine由于是变长栈,初始化栈内存仅2KB,可以大量创建。
它与Couroutine协程也有区别,能够在发现堵塞后启动新的微线程。
通道channel:
类似Unix的Pipe,用于协程之间通讯和同步。
协程之间虽然解耦,但是和它们Channel有着耦合。
# golang中的channel分为有缓存channel和无缓冲channel
2 主要的不同点
2.1 关于消息的发送者和接受者
Actor:注重的处理单元,也就是Actor,而不是消息传送方式。发送消息时,都需要知道对方是谁。
当ActorX要给ActorY发消息时,必须明确知道ActorY的地址。
ActorY接收到消息时,就能够知道消息发送者(ActorX)的地址。
返回消息给发送者时,只需要按发送者的地址往回传消息就行。
CSP:注重的是消息传送方式(channel),不关心发送的人和接收的人是谁。
2.2 消息传输方式
Actor:每一对Actor之间,都有一个“MailBox”来进行收发消息。消息的收发是异步的。
CSP:使用定义的 channel 进行收发消息。消息的收发是同步的(也可以做成异步的,但是一个有限异步)
Actor 模式消息传输,只有一个通道(MailBox),所以无论什么“类型”的消息都可能发过来,所以要做好模式配置。
CSP 中的通道(channel)类型是开发者预先设定好的,两个对象可以可以使用多个通道传输消息。
CSP 把通信给细化了,让你在通信时有多种选择例如:
用一个 channel 传一类数据,用另一个 channel 传另一类数据进行不同处理
2.3 关于同步方式和缓冲
Actor理论上需要一个无限大小的邮箱作为消息缓冲
CSP的Channel 可以完全不需要缓冲消息
用Actor发送是异步的。无论读取器是否准备好从mailbox取出,消息发送方都不会阻塞,而是将消息放入通常称mailbox的队列中。
CSP是完全同步的。通道写入器必须阻塞,直到通道读取器读取为止。这种基于阻塞的机制的优点是,一个通道只需要保存一条消息。
CSP中,发送和接收操作可能会阻塞。在Actor模型中,只有接收操作可能阻塞
# PS:golang中的channel也可以自己设置固定大小的缓冲区
# Go中channel是有容量限制并且独立于处理Goroutine,而如Erlang, Actor模式中的mailbox容量是无限的,接收进程也总是被动地处理消息
3 总结
Actor需要一个容量无限的mailbox,而CSP的Channel 可以完全不需要缓冲消息或设置有限的缓冲
CSP允许通过使用chnnel通道避免生产者和消费者之间的耦合;他们不需要了解对方
看起来Actor更像函数调用,需要知道函数名,而CSP更类似于MQ消息队列
不过Actor更适用于多机器分布式并发场景,CSP更适用于单台机器并发
参考文献:
https://www.freesion.com/article/9011316606/
https://www.jianshu.com/p/6305c86f7754
https://zhuanlan.zhihu.com/p/455843256
知止而后有定,定而后能静,静而后能安,安而后能虑,虑而后能得。
所谓诚其意者,毋自欺也。