【专家坐堂】四种并发编程模型简介

本文来自网易云社区

概述

并发每每和并行一块儿被说起,可是咱们应该明确的是“并发”不等同于“并行”html

•       并发 :同一时间 对待 多件事情 (逻辑层面)java

•       并行 :同一时间 作(执行) 多件事情 (物理层面)golang

并发能够构造出一种问题解决方法,该方法可以被用于并行化,从而让本来只能串行处理的事务并行化,更好地发挥出当前多核CPU,分布式集群的能力。web

可是,并发编程和人们正常的思惟方式是不同的,所以才有了各类编程模型的抽象来帮助咱们更方便,更不容易出错的方式构建并发程序。下面将简单介绍一些常见的并发编程模型,但愿能帮助你们对并发编程有更多的兴趣。这些模型都有各自的优点,须要根据应用场景挑选,而挑选的前提是可以深刻地理解它们。编程

多线程编程模型

多线程模型是用于处理并发的最通用手段,在 C/C++/JAVA 等语言中普遍存在。主要特性有:设计模式

l  多个相互独立的执行流.多线程

l  共享内存(状态).并发

l  抢占式的调度.框架

l  依赖锁,信号量等同步机制异步

多线程程序容易编写(由于写的是顺序程序),可是难分析,难调试,更容易出错,常见的有竞争条件,死锁,活锁,资源耗尽,优先级反转… 等等。

为了下降多线程模型编写难度,不少语言都一直在不断地引入并发编程方面新的特性,例如Java。从最先1996年的JDK1.0 版本起就已经有了Thread,Runnable类,确立了最基础的线程模型,这已经比直接调用POSIX接口构建多线程应用的方式有了很大的提升。而后在JDK5时引入了java.util.concurrent包,其中的线程池(Thread Pool,Executors)等类库,使得Java并发编程的易用性有了更好的提高。

到了JDK7, Fork/Join框架被引入,虽然底层同样是基于ExecutorService线程池的实现。但在编写并发逻辑时会比传统多线程方式更加直观,开发者能够将一个大的做业抽象为几个能够并发的子任务的结果整合;而每一个子任务又能够继续按此逻辑继续划分,充分发挥现代多核CPU的性能。

同时,Fork/Join框架中还内置了Work-Stealing的任务调度机制,可以在尽可能下降线程竞争的同时尝试自动均衡各工做线程之间的任务负载。以下图所示: 


Ø  4个线程每一个都有独立的工做队列,避免单任务队列竞争

Ø  队列中的任务采用相似LIFO方式进出。因为总体做业都是按照一个大任务fork出多个子任务来抽象,所以能够视为越大粒度的任务会沉在队列的越底部。

Ø  当某个线程(示例中为线程D)的工做队列为空时,该线程就会自动尝试从另外一个线程(示例中为线程A)的队列底部”偷“一个任务过来执行。因为是从底部窃取的任务,能够假设这个任务将展开更多的子任务,从而减小窃取动做的产生,下降线程争用频率。

经过这些手段,Fork/Join框架能帮助开发者无需在考虑手动实现并发任务执行时的高效同步逻辑。


随后,JDK8中又引入了并行流(Parallel Streams)的概念, 该特性基于Fork/Join框架,但在易用性方面继续有所提高。并行流采用共享线程池的思路,从而连线程/线程池的配置逻辑都帮开发者简化了。固然,正是由于这个共享池( ForkJoinPool.commonPool() )是被JVM管理,同时被JVM内的全部线程共享,也致使了一些隐患,若是开发者并无了解并行流的底层实现机制,则可能致使应用中利用到并行流的任务产生停滞现象。例以下面的代码示例:


因为 WS.url(url).get()会触发HTTP请求,所以执行到这一句代码时,线程池会被阻塞在IO操做上,结果致使了当前JVM中全部并行流中的任务所有被阻塞。

Callback编程模型

“回调”是一个很容易理解的名词。简单来讲:某个函数(A)能够接受另外一个函数(B)做为参数,在执行流程到某个点时做为参数的函数B就会被函数A调用执行,这个行为就被称为回调。


现实中,回调经常用于异步事件。即,函数A通常会在函数B没有被调用的状况下就先返回,而在某个异步事件发生时再触发调用函数B。


可是滥用回调嵌套,就会致使著名的”callback hell”问题,代码难以阅读和维护。例以下面的片断:


为了不此类大坑,咱们能够参考如下几类解决方案:


l  Promises/A+规范: 它是一种用于管理异步回调的代码结构和流程,一种回调的语法糖。能够把本来嵌套的回调函数展平,使得代码逻辑更清楚。例如片断:


l  Generator: 生成器/半协程方式: 能够将一个函数执行暂停,并保存上下文, 将控制权交还给调用者;当再次被调用时,可以恢复当时的暂停状态继续执行。因此generator函数的行为表现和迭代器很相似,每次触发它的时候能够获取到新的结果,而不是像传统函数所有执行结束后一口气返回一系列值。 代码片断:


l  Async/Await:  能够视为Generator方式的语法糖,可以更好地展现异步调用的语义: async关键字用于表示该函数中有异步操做;await关键字表示须要等待(异步方式)后继表达式的结果。


Actor编程模型

Actor模型首先是由Carl Hewitt在1973年提出定义, 随后由Erlang OTP (Open Telecom Platform) 推广开来。Actor属于并发组件模型, 经过组件方式定义并发编程范式的高级阶段,避免使用者直接接触多线程并发或线程池等基础概念,其消息传递更加符合面向对象的原始意图。

传统多数流行的语言并发是基于多线程之间的共享内存,使用同步机制来防止写争夺。而Actors使用消息模型,每一个Actors在同一时间处理最多一个消息,能够发送消息给其余Actors,保证了单独写原则,从而巧妙避免了多线程的写争夺。

Actor模型不只仅对于单机的并发应用开发有意义,对于分布式应用的开发也是一个能够大展手脚的场景: 节点之间互相独立,只能靠消息通信,异步消息避免节点瓶颈等特性都很是贴合Actor模型的使用。

Actor模型的特色是:

l  万物皆是Actor

l  Actor之间彻底独立,只容许消息传递,不容许其余”任何”共享

l  每一个Actor最多同时只能进行同样工做

l  每一个Actor都有一个专属的命名Mailbox(非匿名)

l  消息的传递是彻底异步的;


l  消息是不可变的

在Java中,能够利用Akka进行Actor编程模型的应用开发。Akka 将自身定义为一套用于构建JVM上高并发,容错式,分布式,消息驱动特性应用开发的工具包和运行环境。详细介绍可参见官网: http://akka.io/

下面用代码片断来展现下基于AKKA开发示例:

咱们定义了两个Actor: HelloWorld 和 Greeter.

l  HelloWorld会处理几个消息

n  启动消息(能够将preStart方法的调用视为收到一个专属启动事件的处理): 主动向Greeter(ActorRef能够视为对应Actor的专属Mailbox)发送一个Msg.GREET消息

n  Msg.Done消息: 接收完该消息后,中止当前Actor

n  其余消息: 调用unhandled() 处理

l  Greeter会处理这些消息:

n  Msg.GREET消息: 向System.out输出字符串, 并向消息的发送者回复一个Msg.Done消息

n  其余消息: 调用unhandled() 处理


HelloWorld,Greeter能够根据须要实例化在多个线程中执行,编码过程当中不须要考虑传统多线程中的Lock/Wait/Notify等同步手段就能让这两个Actor之间分别指示对方完成相应动做。

 

CSP编程模型

CSP(Communicating Sequential Processes)是由Tony Hoare在1978的论文上首次提出的。 它是处理并发编程的一种设计模式或者模型,指导并发程序的设计,提供了一种并发程序可实践的组织方法或者设计范式。经过此方法,能够减小并发程序引入的其它缺点,减小和规避并发程序的常见缺点和bug,而且能够被数学理论所论证。

CSP将程序分红两种模块,Processor 与 Channel:Processor 表明了执行任务的顺序单元,它们内部没有并发,而Channel表明了并发流之间的信息交互,如共享数据的交换、修改、消息传递等等。

除了Channel,Processor之间再无联系,这样就将并发同步做用缩小在Channel之处,使得问题获得了约束、集中。同步操做与争用并无消失,只是聚焦在Channel之上。Processor之间的协做,Channel提供原语来支持,如Barrier等。

CSP 的好处是使得系统较为清晰,Processor 之间是解耦合的,职责也很是清楚,容易理解和维护。

l  工做者之间不直接进行通讯

l  工做者向不一样的通道中发布本身的消息(事件)。其余工做者们能够在这些通道上监听消息,发送者不知道具体谁在执行(匿名)


l  消息交互是同步方式

在Java中对于CSP模型的实现库有JCSP。 同时在JDK中的SynchronousQueue,和CSP中的Channel有殊途同归之妙。Executors.newCachedThreadPool()中就利用到了SynchronousQueue,任务提交者是并不清楚底层哪一个线程会处理提交的任务,而且当提交任务操做完成时必然已经有某个线程接受了该任务(并不表明线程开始执行),所以提交操做此次消息交互是同步的方式。这和Executors.newFixedThreadPool()之类建立的线程池是大相径庭的,其余线程池在提交操做完成时,任务分配给线程这个动做是异步的。

此外,Go语言内置的goroutines & channels并发模型就是参考了CSP的思想,所以Go的并发编程强调不要利用共享内存来进行线程通信,而应该依靠通信来共享数据(Do not communicate by sharing memory; instead, share memory by communicating),尽可能避免锁和线程争用。

参考资料

http://web.stanford.edu/~ouster/cgi-bin/papers/threads.pdf

https://en.wikipedia.org/wiki/Actor_model

https://en.wikipedia.org/wiki/Communicating_sequential_processes

https://talks.golang.org/2012/waza.slide#1

https://www.quora.com/What-are-the-differences-between-parallel-concurrent-and-asynchronous-programming

http://wiki.commonjs.org/wiki/Promises/A

http://www.ibm.com/developerworks/cn/java/j-csp1.html

http://blog.takipi.com/forkjoin-framework-vs-parallel-streams-vs-executorservice-the-ultimate-benchmark/

https://www.cs.kent.ac.uk/projects/ofa/jcsp/cpa2007-jcsp.pdf

http://tutorials.jenkov.com/java-concurrency/index.html

http://www.raychase.net/698

 


原文:【专家坐堂】四种并发编程模型简介

网易云新用户大礼包:https://www.163yun.com/gift

本文来自网易云社区,经做者邱晟受权发布。

相关文章
相关标签/搜索