本篇文章咱们来探讨一下并发设计模型。java
可使用不一样的并发模型来实现并发系统,并发模型说的是系统中的线程如何协做完成并发任务。不一样的并发模型以不一样的方式拆分任务,线程能够以不一样的方式进行通讯和协做。程序员
并发模型其实和分布式系统模型很是类似,在并发模型中是线程
彼此进行通讯,而在分布式系统模型中是 进程
彼此进行通讯。然而本质上,进程和线程也很是类似。这也就是为何并发模型和分布式模型很是类似的缘由。算法
分布式系统一般要比并发系统面临更多的挑战和问题好比进程通讯、网络可能出现异常,或者远程机器挂掉等等。可是一个并发模型一样面临着好比 CPU 故障、网卡出现问题、硬盘出现问题等。数据库
由于并发模型和分布式模型很类似,所以他们能够相互借鉴,例如用于线程分配的模型就相似于分布式系统环境中的负载均衡模型。编程
其实说白了,分布式模型的思想就是借鉴并发模型的基础上推演发展来的。数组
并发模型的一个重要的方面是,线程是否应该共享状态
,是具备共享状态
仍是独立状态
。共享状态也就意味着在不一样线程之间共享某些状态缓存
状态其实就是数据
,好比一个或者多个对象。当线程要共享数据时,就会形成 竞态条件
或者 死锁
等问题。固然,这些问题只是可能会出现,具体实现方式取决于你是否安全的使用和访问共享对象。安全
独立的状态代表状态不会在多个线程之间共享,若是线程之间须要通讯的话,他们能够访问不可变的对象来实现,这是一种最有效的避免并发问题的一种方式,以下图所示网络
使用独立状态让咱们的设计更加简单,由于只有一个线程可以访问对象,即便交换对象,也是不可变的对象。数据结构
第一个并发模型是并行 worker 模型,客户端会把任务交给 代理人(Delegator)
,而后由代理人把工做分配给不一样的 工人(worker)
。以下图所示
并行 worker 的核心思想是,它主要有两个进程即代理人和工人,Delegator 负责接收来自客户端的任务并把任务下发,交给具体的 Worker 进行处理,Worker 处理完成后把结果返回给 Delegator,在 Delegator 接收到 Worker 处理的结果后对其进行汇总,而后交给客户端。
并行 Worker 模型是 Java 并发模型中很是常见的一种模型。许多 java.util.concurrent
包下的并发工具都使用了这种模型。
并行 Worker 模型的一个很是明显的特色就是很容易理解,为了提升系统的并行度你能够增长多个 Worker 完成任务。
并行 Worker 模型的另一个好处就是,它会将一个任务拆分红多个小任务,并发执行,Delegator 在接受到 Worker 的处理结果后就会返回给 Client,整个 Worker -> Delegator -> Client 的过程是异步
的。
一样的,并行 Worker 模式一样会有一些隐藏的缺点
共享状态会变得很复杂
实际的并行 Worker 要比咱们图中画出的更复杂,主要是并行 Worker 一般会访问内存或共享数据库中的某些共享数据。
这些共享状态可能会使用一些工做队列来保存业务数据、数据缓存、数据库的链接池等。在线程通讯中,线程须要确保共享状态是否可以让其余线程共享,而不是仅仅停留在 CPU 缓存中让本身可用,固然这些都是程序员在设计时就须要考虑的问题。线程须要避免 竞态条件
,死锁
和许多其余共享状态形成的并发问题。
多线程在访问共享数据时,会丢失并发性,由于操做系统要保证只有一个线程可以访问数据,这会致使共享数据的争用和抢占。未抢占到资源的线程会 阻塞
。
现代的非阻塞并发算法能够减小争用提升性能,可是非阻塞算法比较难以实现。
可持久化的数据结构(Persistent data structures)
是另一个选择。可持久化的数据结构在修改后始终会保留先前版本。所以,若是多个线程同时修改一个可持久化的数据结构,而且一个线程对其进行了修改,则修改的线程会得到对新数据结构的引用。
虽然可持久化的数据结构是一个新的解决方法,可是这种方法实行起来却有一些问题,好比,一个持久列表会将新元素添加到列表的开头,并返回所添加的新元素的引用,可是其余线程仍然只持有列表中先前的第一个元素的引用,他们看不到新添加的元素。
持久化的数据结构好比 链表(LinkedList)
在硬件性能上表现不佳。列表中的每一个元素都是一个对象,这些对象散布在计算机内存中。现代 CPU 的顺序访问每每要快的多,所以使用数组等顺序访问的数据结构则可以得到更高的性能。CPU 高速缓存能够将一个大的矩阵块加载到高速缓存中,并让 CPU 在加载后直接访问 CPU 高速缓存中的数据。对于链表,将元素分散在整个 RAM 上,这其实是不可能的。
无状态的 worker
共享状态能够由其余线程所修改,所以,worker 必须在每次操做共享状态时从新读取,以确保在副本上可以正确工做。不在线程内部保持状态的 worker 成为无状态的 worker。
做业顺序是不肯定的
并行工做模型的另外一个缺点是做业的顺序不肯定,没法保证首先执行或最后执行哪些做业。任务 A 在任务 B 以前分配给 worker,可是任务 B 可能在任务 A 以前执行。
第二种并发模型就是咱们常常在生产车间遇到的 流水线并发模型
,下面是流水线设计模型的流程图
这种组织架构就像是工厂中装配线中的 worker,每一个 worker 只完成所有工做的一部分,完成一部分后,worker 会将工做转发给下一个 worker。
每道程序都在本身的线程中运行,彼此之间不会共享状态,这种模型也被称为无共享并发模型。
使用流水线并发模型一般被设计为非阻塞I/O
,也就是说,当没有给 worker 分配任务时,worker 会作其余工做。非阻塞I/O 意味着当 worker 开始 I/O 操做,例如从网络中读取文件,worker 不会等待 I/O 调用完成。由于 I/O 操做很慢,因此等待 I/O 很是耗费时间。在等待 I/O 的同时,CPU 能够作其余事情,I/O 操做完成后的结果将传递给下一个 worker。下面是非阻塞 I/O 的流程图
在实际状况中,任务一般不会按着一条装配线流动,因为大多数程序须要作不少事情,所以须要根据完成的不一样工做在不一样的 worker 之间流动,以下图所示
任务还可能须要多个 worker 共同参与完成
使用流水线模型的系统有时也被称为 响应式
或者 事件驱动系统
,这种模型会根据外部的事件做出响应,事件多是某个 HTTP 请求或者某个文件完成加载到内存中。
在 Actor 模型中,每个 Actor 其实就是一个 Worker, 每个 Actor 都可以处理任务。
简单来讲,Actor 模型是一个并发模型,它定义了一系列系统组件应该如何动做和交互的通用规则,最著名的使用这套规则的编程语言是 Erlang。一个参与者Actor
对接收到的消息作出响应,而后能够建立出更多的 Actor 或发送更多的消息,同时准备接收下一条消息。
在 Channel 模型中,worker 一般不会直接通讯,与此相对的,他们一般将事件发送到不一样的 通道(Channel)
上,而后其余 worker 能够在这些通道上获取消息,下面是 Channel 的模型图
有的时候 worker 不须要明确知道接下来的 worker 是谁,他们只须要将做者写入通道中,监听 Channel 的 worker 能够订阅或者取消订阅,这种方式下降了 worker 和 worker 之间的耦合性。
与并行设计模型相比,流水线模型具备一些优点,具体优点以下
不会存在共享状态
由于流水线设计可以保证 worker 在处理完成后再传递给下一个 worker,因此 worker 与 worker 之间不须要共享任何状态,也就不用无需考虑觉得并发而引发的并发问题。你甚至能够在实现上把每一个 worker 当作是单线程的一种。
有状态 worker
由于 worker 知道没有其余线程修改自身的数据,因此流水线设计中的 worker 是有状态的,有状态的意思是他们能够将须要操做的数据保留在内存中,有状态一般比无状态更快。
更好的硬件整合
由于你能够把流水线当作是单线程的,而单线程的工做优点在于它可以和硬件的工做方式相同。由于有状态的 worker 一般在 CPU 中缓存数据,这样能够更快地访问缓存的数据。
使任务更加有效的进行
能够对流水线并发模型中的任务进行排序,通常用来日志的写入和恢复。
流水线并发模型的缺点是任务会涉及多个 worker,所以可能会分散在项目代码的多个类中。所以很难肯定每一个 worker 都在执行哪一个任务。流水线的代码编写也比较困难,设计许多嵌套回调处理程序的代码一般被称为 回调地狱
。回调地狱很难追踪 debug。
函数性并行模型是最近才提出的一种并发模型,它的基本思路是使用函数调用来实现。消息的传递就至关因而函数的调用。传递给函数的参数都会被拷贝,所以在函数以外的任何实体都没法操纵函数内的数据。这使得函数执行相似于原子
操做。每一个函数调用均可以独立于任何其余函数调用执行。
当每一个函数调用独立执行时,每一个函数均可以在单独的 CPU 上执行。这也就是说,函数式并行并行至关因而各个 CPU 单独执行各自的任务。
JDK 1.7 中的 ForkAndJoinPool
类就实现了函数性并行的功能。Java 8 提出了 stream 的概念,使用并行流也可以实现大量集合的迭代。
函数性并行的难点是要知道函数的调用流程以及哪些 CPU 执行了哪些函数,跨 CPU 函数调用会带来额外的开销。
做者:cxuan本文版权归做者和博客园共有,未经做者容许不能转载,不然追究法律责任的权利。 若是文中有什么错误,欢迎指出。以避免更多的人被误导。 |