并发体系可以分为两层,上面一层是抽象的模型,下面一层是承载具体模型的技术机制。
模型可以分为: 共享内存模型、Actor模型、CSP模型等
承载模型的具体技术机制可以分为: 协程、多线程、多进程、集群分布式
常见的并发模型一般包括3类,基于线程与锁的内存共享模型,actor模型和CSP模型。
Actor和CSP模型都属于高层次的抽象模型,底层的实现可能有多种。而线程与锁(也就是共享内存)模型是较为底层的模型。
一、传统多进程。
多进程模型是利用操作系统的进程模型来实现并发的。典型的是Apache Web Server,每个用户请求接入的时候都会创建一个进程,这样应用就可以同时支持多个用户。
在多进程中如果不同进程想共享内存中的数据必须通过进程间通信的方式来实现。
缺点:创建进程的开销非常高,如果进程过多会很快消耗光系统资源。并且上下文切换在进程间的开销也很高。
在I/O密集型任务的应用中,此并发模型很难满足需求。
二、传统多线程+共享内存通信。
相比多进程模型来说,因为线程比进程创建的系统开销小,所以多线程模型是很常见的实现并发的方式。但此种模型存在一个必须解决的问题,就是线程间通信的问题。但线程为什么要通信呢?那是因为大部分业务系统问题的解空间在用冯·诺伊曼计算机去实现的时候,都存在并发计算时线程间数据共享的问题。要数据共享有两种方式:
- 共享内存通信(Shared memory communication):不同线程间可以访问同一内存地址空间,并可修改此地址空间的数据。
- 消息传递通信(Message passing communication):不同线程间只能通过收发消息的形式去通信,数据只能被拥有它的线程修改。
传统多线程的编程方式是直接控制thread,使用共享内存(ShareMemory)模型。但是共享内存资源(不止内存、只要共享了资源都可能会出现竞争),会出现竞态条件(即代码的行为取决于各操作的时序),导致线程不安全,最终程序运行结果错误或者崩溃。
解决竞态条件的方式是对数据进行同步(Synchronize)访问。要实现同步访问常见的方式有:
- 使用Semaphores,lock,condition等同步原语来强行规定进程的执行顺序。
- 同步屏障(Barriers):通过设置屏障控制不同线程执行周期实现同步访问
- CAS、原子变量:Atomic,以及基于CAS等操作设计的无锁数据结构
锁模型
锁模型本身是在并发单元复用共享资源时,解决竞争的一种手段。
并发单元可以是进程也可以是线程,因此多线程模型中常用锁,但是多进程模型中也可能会用锁。
锁本身又是一个复杂的东西,它有很多种类:
按性能维度分:自旋锁/阻塞锁、偏向/轻量级/重量锁
按模型分:乐观/悲观锁/独享/共享锁/分段锁
按调度分:公平/非公平锁
按接口实用性分:递归锁/可重入锁
无论有哪些种类,从完备性说,经典编程中实现任何锁模型只需要两个东西:互斥锁+条件变量。
STM模型
锁模型是一种悲观的并发同步机制,但实际上冲突发生的概率并不高,所以乐观的并发同步机制性能更好。
STM(Software transactional memory)就是这样一种用来代替锁模型的乐观并发同步机制。
STM是用软件的方式去实现事务内存(Transactional memory),而事务内存中的事务(Transactional)正是关系型数据库中的概念,一个事务必须满足ACID性质,如下图所示:
- 在t0时刻,T1、T2与T3线程同时获取要操作的同一数据的快照v0
- 之后T1线程在自己的事务里于t1时刻提交自己的写入值v1
- 之后T2线程在自己的事务里提交自己的写入值v2。由于在提交时刻会做冲突检测,此事务发现操作数据的快照已经发生变化,于是回滚自己的提交。
- 之后开启新的事务重新获取最新的快照v1,并于时刻t2成功提交自己的写入值v2。
- 在线程v3中由于是读取操作,并没有数据修改,所以在它的事务中使用的是最早的快照v0。
在STM的事务中尽可能避免副作用,比如在事务中去修改原子变量这种操作,可能会导致事务回滚失败。
STM实现的一种方式是基于MVCC(Multiversion concurrency control)。很多编程语言提供了这种并发模型的实现。
三、Actor模型。
一、Actor模型的基本结构。
1、Actor 模型由一个个称为 Actor 的执行体和 mailbox 组成。mailbox一般我们也叫做消息队列。
2、Actor之间通信是通过消息传递完成。用户将消息发送给 Actor,实际上就是将消息放入一个队列中。其它Acotr作为消费者,去消费队列中的消息。
3、消息让Actor之间异步解耦。
从传统多线程编程的思路看Actor模型:
共享的可变状态会引入诸多麻烦。对此有许多解决方法,比如常见的锁,事务,函数式编程中则直接使用了不可变状态。
Actor 模型则允许可变状态,只是只通过消息传递的方式来进行状态的改变。
多个Actor串接起来,就形成流水线:
Actor模型,是基于消息传递的并发模型, 强调的是Actor这个工作实体,每个Actor自行决定消息传递的方向(要传递的ActorB),通过消息传递形成流水线。
作为一种通用并发编程模型,Actor的适用性非常强,举例:
1、共享内存架构,比如一个线程作为一个Actor。
2、分布式内存架构,适合解决地理分布型的问题。同时它还能提供很好的容错性。(CSP模型就无法跨节点在分布式集群运行)
3、C语言的单线线程job调度,也可以使用Actor的思想去解决一些问题。
四、CSP模型。
1、CSP为CSP(communicating sequential process顺序通信过程)。有趣的是它还是代数演算,见 wiki 。
2、与 Actor 模型类似,CSP 模型也是由独立的、并发执行的实体所组成,实体之间也是通过发送消息进行通信。但两种模型的差别是,CSP中channel是作为first-class独立存在的,CSP关注的是Channel,而不关心消息实体。
3、默认情况下的channel是无缓存的, 对channel的send动作是同步阻塞的,直到另外一个持有该channel引用的执行块取出消息(channel为空)。by default时,实际的receive操作只会在send之后才被发生。而Actor中,由于send这个动作是异步的,因此Actor的receive会按照信箱接受到消息的顺序来进行处理。
在CSP模型,worker之间不直接彼此联系,强调信道在消息传递中的作用,不谋求形成流水线。
消息的发送者和接受者通过该信道松耦合,发送者不知道自己消息被哪个接受者消费了,接受者也不知道是从哪个发送者发送的消息。
五、EventLoop+异步回调
Nodejs采用的方式是事件驱动+异步回调的方式,很多异步操作是先投入到事件队列(Event Queue)里面,然后逐个投放到线程池里面执行,执行完再把结果投放回事件队列里面。
这种模式显然不是CSP——他没有channel。也不是Actor,因为他只有一个事件队列。
此模型巧妙的利用了系统内核提供的I/O多路复用系统调用,将多个socket连接转换成一个事件队列(event queue),只需要单个线程即可循环处理这个事件队列。
EventLoop的实现模型来源古老,早期在图形界面GUI编程中大量使用,node的前身JavaScript本身就是服务于浏览器的,可以认为也算一种“图形界面”,自然采取了这种模型。
NodeJs实际的架构后面做了优化,比这个复杂一些,理解原理这个够用了。
这一模型构成了当前NodeJs协程的运行时模型。
参考资料
https://www.51cto.com/article/704926.html
https://www.zhihu.com/question/271713879
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。