32

这些并发模型你真的懂了吗?未必 | Go 技术论坛

 4 years ago
source link: https://learnku.com/articles/32807?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

这些并发模型你真的懂了吗?未必 | Go 技术论坛

/ 858 / 0 / 发布于 1年前 / 更新于 11个月前 / 1 个改进

并发模型简介#

并发:一个人同一时间应对多件事的能力
并行:一个人同一时间处理多件事的能力(显然一个人同一事件不能处理多件事,单核 CPU 不具备并行能力)

可以理解为并行是并发的一种特殊情况
并发模型的核心是为了提高提高 CPU 利用率,提高服务器应对大量请求,海量数据处理的能力,单核 CPU 性能已经难以发展,各大厂商都在通过增加 CPU 个数来达到硬件处理能力的提高(摩尔定律),随之而来在编程语言方面衍生出各个模型(其实就是处理问题的思路)用来压榨硬件的性能,以使自己的系统并发能力得到提高。

同步和异步以及阻塞和非阻塞#

要了解各种并发模型思想,首先要了解什么是同步,什么是异步?什么是阻塞,什么是非阻塞?

举一个例子来说明上面的概念,小明去买自己爱吃的烧鸡
同步阻塞的做法是小明付帐后一直盯着老板制作烧鸡,直到完成才高兴的办其它事了。
同步非阻塞的做法是小明付帐后不会一直盯着老板,而是做其它事了,每隔一会来看看老板做好了没。
异步阻塞的做法是小明付帐以后,不会盯着老板做了,也不干其它事,老板做好了通知小明。
异步非阻塞指的是小明付帐以后,干自己的事去了,老板做好了通知小明。

同步和异步的本质是我轮询你还是你回调我
阻塞和非阻塞的本质是当发生等待的时候我能不能干其它的事

我们用 IO 操作再来来描述一下同步,异步,阻塞,非阻塞的情况
同步 IO :线程发起 read 操作,调用操作系统,这是会有一次用户态切换到内核态,内核开始等待数据到达,数据完成后,从内核拷贝到用户态空间,这个过程线程是一直等待状态的。
阻塞 IO:线程发起 read 操作,然后线程一直处于等待状态,直到 IO 操作完成,其实和上面的同步 IO 一样。
非阻塞 IO:线程发起 read 操作,用户态切换到内核态,如果内核数据没有准备好,立刻返回一个错误,线程根据错误决定每隔一会轮询依次,当内核数据准备好后,会将数据从内核拷贝到用户态空间这个时候线程是一只处于等待状态的,也就是说第一阶段是非阻塞,第二阶段还是阻塞的。
异步 IO:线程发起 read 操作后,便可以做其他事了,操作系统数据准备好后(已经拷贝到用户态)会告诉线程。

进程和线程的区别#

进程是操作系统资源分配的基本单位。
线程是 CPU 调度的基本单位。
从操作系统层面去看是进程,从 CPU 层面去看是线程。

进程的空间是独立,各个进程相互不干扰,每个进程拥有自己的进程内存,上下文环境,进程控制块,一个进程至少有一个或者多个线程。线程属于进程,线程要存在必须依赖于进程,线程共享进程的内存,但线程有自己的栈空间,能创建多少个线程也取决于进程内存的大小。

线程的上下文切换代价比进程要小的多。
进程之间强调的是通信,线程之间强调的是同步(数据安全)。

这些并发模型你真的懂了吗?未必

上面列了一些简单的比较,其实不同操作系统下有着一些较大差别,比如 linux 操作系统下,进程的创建和销毁其实和线程创建和销毁所需的代价差不多,具体需要在使用时深入调研。

从操作系统系统层面考量的并发模型#

1、多进程单线程#

这种并发模型是应用程序启动后主进程会预先创建一些子进程出来,每来一个请求都会由一个子进程处理请求,这种模型会比较稳定,进程之间不干扰,也不会产生线程安全问题,同时也可以引入一些第三方的非线程安全的模块进来,但内存消耗较大,创建进程对内存的消耗会比较大,并且 cpu 在多个进程间来回切换开销也大,所以一般子进程不宜过多。典型的一些开源软件如 Apache 服务器在 Apahce2.X 之后新增了并行处理模块(MPM->Multi-Processing-Modules)Prefork 就是这种并发模型

2、多进程多线程#

这种并发模型是在上面多进程的并发模型上演化而来,开启多个子进程,每个子进程下面又会开启多个线程,这种模式下并发承受压力会比单纯的多进程好许多,但在一些 CPU 密集型作业下未必会比多进程好,因为每一个进程下的多线程上下文不断切换的开销是非常大的,cpu 本来就在多个进程间切换,现在又要在单个进程下的多个线程间切换,cpu 大部分时间都在切换上下文了,真正用于计算的时间反而很少,因此影响了其性能,因此对于一些网页请求或者偏 IO 类的操作这种模式会比多进程的好上一些,典型的一些开源软件如 Apache 服务器在 Apahce2.X 之后新增了并行处理模块(MPM->Multi-Processing-Modules)Worker 就是这种并发模型

3、单进程多线程#

这种并发模型也是现在大多 web 后台开发的一种模式,尤其在 Java 中,应用程序启动后开启主线程,之后的请求都通过线程池技术来支撑并发。操作系统能保证当线程数小于等于 cpu 的个数时,让不同的线程运行在不同的 cpu 上,提高 cpu 的利用率,典型的如开源框架 tomcat 就是这种并发模型。

从编码层面 (各种框架) 设计的并发模型#

1、reactor 模型#

传统的基于多线程的 client-server 模式,客户端每发送一个请求,server 就开启一个线程处理客户端请求,这种模式在并发量不是很大的情况下非常好,性能 OK,编码也简单,但当并发量一旦突破上线,性能就会急剧下降,占用更多内存,cpu 频繁的在多个线程间进行上下文切换,reactor 模式是基于事件驱动的高并发模型,他把一次请求分成多个事件,比如(connect,read,write),每次事件发生的时候才去触发对应的处理器处理,reactor 架构的主要由以下几个组件组成

  1. Handle (window 中称为句柄,linux 中称为文件描述符,比如一个网络 socket,在这个 Handle 上可以发生很多事件,比如 connect,read,write),
  2. SynchronousEventDemultiplexer (同步事件分离器,本质上是系统调用)
  3. EventHandler (事件处理接口),
  4. Concrete Event Handler (实现应用程序所特提供的特定事件处理接口),
  5. Reactor(反应器,循环运行事件,操作事件句柄的增删改查操作,分发事件)

这种模式将请求和处理分离,有专门的 accept 线程监听来自客户端的请求,请求到来后,也有专门的线程池处理读写任务,同时也有对应的业务线程池处理具体的业务逻辑,reactor 模式虽然性能这么高,很多框架也在用,但 reactor 模式是同步的,主要体现在 IO 操作上会阻塞一直等待读写完成,如下图

这些并发模型你真的懂了吗?未必

2、proactor 模型#

proactor 也是基于事件驱动的一种并发模型,但 protacor 是异步的,在 IO 操作时,proactor 并发模型能够和操作系统之间解耦,由操作系统内核完成读写操作之后主动发送完成事件,这也是和 reactor 的最大区别,proactor 由以下几个组件组成:

  1. Handle(句柄)
  2. AsynchronousOperationProcessor(异步事件处理器)
  3. Asynchronous Operation(异步操作)
  4. Completion Event Queue(完成事件队列,异步操作的结果放入队列中)
  5. Proactor(主动器,提供完成事件的循环,进行事件分发处理后续逻辑)
  6. Completion Handler(完成事件接口)
  7. Concrete Completion Handler(完成事件业务逻辑,实现上面的事件接口)

这种模式下真正实现 IO 的异步操作,不发发生阻塞,其实观察 reactor 和 proactor 并发模型,发现都是尽量减少线程在执行期间的阻塞,将原本在一条直线上完成的所有操作分割成多端,之间通过事件进行通信,reactor 注册的是就绪事件,而 proactor 注册的是完成事件,由一个统一中央事件分发器进行管理,协作,这两种模型都依赖操作系统内核本身的支持,框架只是在操作系统本身的支持下调用操作系统的 api 实现了更高一层的封装,proactor 模型如下图:

这些并发模型你真的懂了吗?未必

3、actor 模型#

不管任何并发模型其实都离不开的数据之间的交互,都需要通信,reactor,proactor 这两种模型都是通过共享内存来进行通信,而 actor 强调的是通过通信来共享内存,actor 强调的是没有共享,所有的线程之间都是消息传递来实现通信,数据交互,每一个 actor 就是一个线程,actor 模型几十年前就已经出现,但因为受制于当时硬件的发展并没有被重视,随着多核时代的到来,actor 模型开始有了用武之地,其中 golang 的 goroutine,channel 就是 actor 模型的一种实现,actor 模型更适合多核编程,分布式编程,actor 模型通过消息传递保证了内部数据的状态只会由自己修改,所以内部数据的处理不会涉及到锁,同步等问题,actor 模型由以下几个组件组成:

  1. state (状态,状态由 actor 自身内部维护)
  2. Behavior (行为,指的是 actor 中计算逻辑或者业务逻辑)
  3. MailBox (邮箱,邮箱是 actor 和 actor 之间通信的桥梁)

actor 模型主要解决的是并发编程带来的锁,同步等复杂性,事实上 MailBox 中也有锁,同步的逻辑,试想一下,两个 actor 通过 MailBox 进行通信,一个写,一个读,就会有并发问题,actor 模型也是做了更高层次的抽象,封装,我们从编程角度或者架构角度来看 actor 是实现通过消息传递来共享数据的模型设计,如下图:

这些并发模型你真的懂了吗?未必

Golang
本作品采用《CC 协议》,转载必须注明作者和本文链接
那小子阿伟

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK