文章目录
一.什么是并发模型
并发模型是一种用于描述和分析并发系统的数学模型
并发模型主要包含IO多路复用,多进程以及多线程
二.为什么要学习并发模型?
不同语言的并发编程不同的根本原因在于其底层的并发模型不一样.
随着信息技术的高速发展,迫使编程模式由从前的串行模型升级到并发模型.
现代复杂的高并发架构大多是IO多路复用,多进程,多线程几种模型协同使用,不同场景运用不同模型,
发挥出服务器的最大性能.
再补充一下几个概念:
- 进程:进程是系统进行资源分配的基本单位,有独立的内存空间。
- 线程:线程是 CPU 调度和分派的基本单位,线程依附于进程存在,每个线程会共享父进程的资源。
- 协程:协程是一种用户态的轻量级线程,协程的调度完全由用户控制,协程间切换只需要保存任务的上下文,没有内核的开销。
二.常见得并发模型
1.多进程模型
多进程模型是利用操作系统的并发模型来实现并发的,典型的是Apache Web Server,每个用户请求都会通过fork()系统去创建一个进程.
多线程模型的缺点在于其创建进程的开销非常高,并且多进程过高会很快消耗光系统资源,进程的上下文切换开销也非常高,在I/O密集型任务的应用中,此模型很难满足需求.
2.多线程模型
多线程模型因为其轻量和易用,是并发编程中使用频率最高的并发模型,包括之后衍生的协程等其他子产品,也都基于它。
但此种模型存在一个必须解决的问题,就是线程间通信的问题。但线程为什么要通信呢?那是因为大部分业务系统问题的解决 都存在并发计算时线程间数据共享的问题。要数据共享有两种方式:
-
共享内存通信(Shared memory communication):不同线程间可以访问同一内存地址空间,并可修改此地址空间的数据。
-
消息传递通信(Message passing communication):不同线程间只能通过收发消息的形式去通信,数据只能被拥有它的线程修改。
共享内存通信
因为线程间共享内存资源,在访问临界区域时会出现数据竞争(发生竞态条件,即代码的行为取决于各操作的时序)的问题,如果不能正确的处理此问题,程序会产生线程不安全的问题,最终导致程序崩溃或无法正常运行。
解决竞态条件的方式是对数据进行同步(Synchronize)访问。要实现同步访问常见的方式有:
锁(Lock):通过锁定临界区域来实现同步访问。
信号量(Semaphores):可以通过信号量的增减控制对一个或多个线程对临界区域的访问。
同步屏障(Barriers):通过设置屏障控制不同线程执行周期实现同步访问。
(1)Lock
锁(Lock),也叫互斥量(Mutex)。线程在操作临界区域资源时,需要先获取锁,然后才能操作,当操作完成后,需要释放锁。此模型利用了对底层硬件运行过程的形式化,这让其即简单又复杂。从锁的种类就可以看出来其复杂性:
自旋锁
递归锁
乐观/悲观锁
公平/非公平锁
独享/共享锁
偏向/轻量级/重量锁
分段锁
对锁的使用不当还会产生死锁问题(Deadlock)。在实际开发过程中,能不用锁就不用锁,可以考虑使用一些轻量级的替代方案如原子变量(Atomic),或无锁(lock-free)非阻塞(non-blocking)算法实现的数据结构。
原子变量的更新为何是线程安全的?因为CPU提供了CAS(Compare-and-swap)的指令来更新原子变量,这条指令从硬件上确保了此操作是线程安全的。
此模型的优点:
- 大多编程语言都支持此模型;
- 贴近硬件架构,使用得当性能很高;
- 是其他并发模型的基础;
此模型的缺点:
- 不支持分布式内存模型,只解决了进程内的并发同步;
- 不好调试与测试,想用好不容易;
(2)CSP
通信顺序进程(CSP(Communicating sequential processes))是一种形式语言,用来描述基于消息传递通信的安全并发模型。如下图所示:
这些任务块之间的通信是基于通道(Channel)来完成的,当创建了一个通道之后,不同的任务块就可以通过持有这个通道来通信,通道可以被不同的任务块共享。通道两端任务块的通信可以是同步的,也可以是异步的。
在这里的任务块不是如Java里重量级的线程类,在运行时是非常轻量级的代码块。其实是协程,这些代码块可以被调度到不同的线程中,最终被多个CPU内核并发执行。
此模型的优点:
- 相比锁模型更简单;
- 很容易实现高并发;
此模型的缺点:
- 不支持分布式内存模型,只解决了进程内的并发同步;
(3)Actor
演员模型(Actor)是一种类似面向对象编程思想的安全并发模型。在面向对象的世界里,对象是一种封装了状态及行为的实体,对象间通过消息去通信(通过对象调用其方法)。而在Actor模型中,一切皆Actor,每个Actor中都有自己的状态,其他Actor只能通过通信的方式来获取或修改被通信Actor的状态。Actor通信的方式类似收发邮件,它有自己的收件箱,如下图所示:
在上述图中,我们可以看到相比CSP模型,Actor模型可以跨节点在分布式集群中运行。实际上Actor模型的代表Erlang正是天然分布式容错的编程语言。
此模型的优点:
- 相比锁模型更简单;
- 很容易实现高并发;
- 支持分布式内存模型,能实现跨节点的并发同步;
此模型的缺点:
- 存在信箱满后消息丢失的问题;
3.事件驱动
在多线程方式实现的并发模型中,我们解决问题的方式是通过创建更多的线程来提高系统的并发处理能力。但线程创建的开销及线程间上下文调度切换的开销并不是很小,所以纵使系统的硬件资源很充足,也存在一定的上限。那么有没有可能只创建一个线程,而且这个线程可以同时处理很多个任务呢?当然是可以的,这正是基于I/O多路复用的事件循环处理并发模型的解法,通过单线程来并发处理I/O密集型的任务。
Less is more.
(1).IO多路复用.
此模型巧妙的利用了系统内核提供的I/O多路复用系统调用,将多个socket连接转换成一个事件队列(event queue),只需要单个线程即可循环处理这个事件队列。当然这个线程是有可能被阻塞或长期占用的,针对这种类型的任务处理可以单独使用一个线程池去做,这样就不会阻塞Event Loop的线程了。
此模型的优点:
- 单线程对系统资源的占用很小;
- 很容易实现高并发;
此模型的缺点:
- 不支持分布式内存模型,只解决了进程内的并发同步;
四.并发模型在实际中得应用
许多编程语言标准库或三方库都已支持上述大多数的并发模型,但因为一些历史原因带来的兼容性问题,开发者的使用体验好坏不一。以下仅简单介绍下各种编程语言标准库对并发模型的实现及流行三方库的扩展支持。
1.Java
Java是一门面向对象的编程语言,标准库对并发的支持是基于共享内存通信的锁模型,因此用Java的标准库来实现高并发是一件非常有挑战的事情,想不踩坑太难。
想深入了解Java的并发模型,可以参考《Java并发编程实战》。
当然基于Java的三方库很多实现了其他并发模型,如:
- Actor: Akka
- Event Loop with Multiplexing
Netty
Nginx-Clojure
2.Go
在Go流行的时期流传着一个故事:一个PHP的普通开发者在一周内学会了Go语言,之后开发出了一个高并发的Web应用,要用Java实现同样的性能,至少需要多年的经验。
暂且不论这个故事是否合理,但它展示了Go语言的两大亮点:
- 语法简单易学;
- 天然支持高并发模型;
Go在语言层面实现了CSP并发模型,因此能让开发者以非常低的成本写出高并发的Web应用。在对CSP并发模型的实现中,Go任务块一般是一个函数,这个函数的调度是由Go语言的调度器来完成,可以被调度在不同的线程中。如果在这个函数中出现了阻塞线程的如网络I/O的操作,调度器会委托给Netpoller去执行,而Netpoller的底层正是对操作系统I/O多路复用技术的封装。
Go高并发的秘诀在于它的G-P-M运行时调度模型,详细的设计可参考这篇文章:Go为什么这么快
3.Erlang/Elixir
Erlang是一门天然分布式、高并发、容错的编程语言,它是Actor并发模型的代表编程语言。Elixir是基于Erlang虚拟机(BEAM)的一种不纯粹的、动态类型的函数式语言。它们自然原生支持Actor并发模型,所以在开发高并发的分布式容错应用时,可以考虑使用Elixir,它强大的并发模型及富有表达力的语法可以提供非常好的开发体验。
Erlang的虚拟机在运行时实现了软实时抢占式调度,详细的信息可参考这篇文章:How Erlang does scheduling。
4.Clojure
Clojure是基于JVM平台的Lisp方言,是不纯粹的、动态类型的函数式语言(这点倒和Elixir类似)。Clojure可以直接调用Java的库,这让其可支持非常多的并发模型,但最有特色的就是它的标准库实现了STM的并发模型,官方提供的异步库core.async也实现了CSP的并发模型。当然还可以通过Nginx-Clojure实现基于I/O多路复用的高并发模型。
今天的分享就到这里了,有问题可以在评论区留言,均会及时回复呀.
我是bling,未来不会太差,只要我们不要太懒就行, 咱们下期见.