【GoLang那点事】深入浅出那些你知道但不理解的并发模型

SunPengWei · · 705 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

**文章比较长,需要一些耐心才能看完** #### 并发模型简介 * 并发:一个人同一时间应对多件事的能力 * 并行:一个人同一时间处理多件事的能力(显然一个人同一事件不能处理多件事,单核CPU不具备并行能力) * 可以理解为并行是并发的一种特殊情况 * 并发模型的核心是为了提高提高CPU利用率,提高服务器应对大量请求,海量数据处理的能力,单核CPU性能已经难以发展,各大厂商都在通过增加CPU个数来达到硬件处理能力的提高(摩尔定律),随之而来在编程语言方面衍生出各个模型(其实就是处理问题的思路)用来压榨硬件的性能,以使自己的系统并发能力得到提高。 #### 同步和异步以及阻塞和非阻塞 * 要了解各种并发模型思想,首先要了解什么是同步,什么是异步?什么是阻塞,什么是非阻塞? * 举一个例子来说明上面的概念,小明去买自己爱吃的烧鸡 * **同步阻塞**的做法是小明付帐后一直盯着老板制作烧鸡,直到完成才高兴的办其它事了 * **同步非阻塞**的做法是小明付帐后不会一直盯着老板,而是做其它事了,每隔一会来看看老板做好了没 * **异步阻塞**的做法是小明付帐以后,不会盯着老板做了,也不干其它事,老板做好了通知小明 * **异步非阻塞**指的是小明付帐以后,干自己的事去了,老板做好了通知小明 * **同步和异步的本质是我轮询你还是你回调我** * **阻塞和非阻塞的本质是当发生等待的时候我能不能干其它的事** > 我们用IO操作再来来描述一下同步,异步,阻塞,非阻塞的情况 * **同步IO** :线程发起read操作,调用操作系统,这是会有一次用户态切换到内核态,内核开始等待数据到达,数据完成后,从内核拷贝到用户态空间,这个过程线程是一直等待状态的 * **阻塞IO**:线程发起read操作,然后线程一直处于等待状态,直到IO操作完成,其实和上面的同步IO一样 * **非阻塞IO**:线程发起read操作,用户态切换到内核态,如果内核数据没有准备好,立刻返回一个错误,线程根据错误决定每隔一会轮询依次,当内核数据准备好后,会将数据从内核拷贝到用户态空间这个时候线程是一只处于等待状态的,也就是说第一阶段是非阻塞,第二阶段还是阻塞的 * **异步IO**:线程发起read操作后,便可以做其他事了,操作系统数据准备好后(已经拷贝到用户态)会告诉线程。 #### 进程和线程的区别 * 进程是操作系统资源分配的基本单位 * 线程是CPU调度的基本单位 * 从操作系统层面去看是进程,从CPU层面去看是线程 * 进程的空间是独立,各个进程相互不干扰,每个进程拥有自己的进程内存,上下文环境,进程控制块,一个进程至少有一个或者多个线程。线程属于进程,线程要存在必须依赖于进程,线程共享进程的内存,但线程有自己的栈空间,能创建多少个线程也取决于进程内存的大小。 * 线程的上下文切换代价比进程要小的多。 * 进程之间强调的是通信,线程之间强调的是同步(数据安全) | 比较内容 |进程 |线程 | | --- | --- | --- | |CPU,内存 |占用cpu和内存更多 |占用内存少,cpu切换简单 | |数据共享合和同步 |数据共享比较复杂,需要通信,同步简单,应为数据是分开的 |数据共享简单,但同步比较复杂,需要锁操作 | |创建和销毁 |进程是重量级的,创建和销毁都比较复杂 |线程是一种轻量级的进程,创建和销毁简单 | |编程和调试 |复杂 |简单 | |可靠想 |进程之间相互独立,不会影响,一个进程挂掉不会影响其它进程 |一个线程挂掉可能导致整个进程都over | * 上面列了一些简单的比较,其实不同操作系统下有着一些较大差别,比如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操作上会阻塞一直等待读写完成,如下图 ![Image.png](https://static.studygolang.com/190819/7ed76375ade90c1d72683822e8cd5209.png) #### 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模型如下图: ![Image [2].png](https://static.studygolang.com/190819/ad097172584948bc7916b4ca2fe0a32c.png) #### 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是实现通过消息传递来共享数据的模型设计,如下图: ![Image [3].png](https://static.studygolang.com/190819/2ca2e1eac4fbaf30072909c4fdbce97a.png) **欢迎大家关注微信公众号:“golang那点事”,更多精彩期待你的到来** ![GoLang公众号.jpg](https://static.studygolang.com/190721/c55fa00b6c19806beda719ee62847c9f.jpg) #### 参考文章 * http://www.laputan.org/pub/sag/proactor.pdf * https://www.cnblogs.com/pinganzi/p/6640729.html * https://www.jianshu.com/p/a115e41e1943 * https://www.cnblogs.com/bitkevin/p/5724410.html

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

705 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传