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

本文来自网易云社区

概述

并发往往和并行一起被提及,但是我们应该明确的是“并发”不等同于“并行”

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

?       并行 :同一时间 做(执行) 多件事情 (物理层面)

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

但是,并发编程和人们正常的思维方式是不一样的,因此才有了各种编程模型的抽象来帮助我们更方便,更不容易出错的方式构建并发程序。下面将简单介绍一些常见的并发编程模型,希望能帮助大家对并发编程有更多的兴趣。这些模型都有各自的优势,需要根据应用场景挑选,而挑选的前提是能够深入地理解它们。

多线程编程模型

多线程模型是用于处理并发的最通用手段,在 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

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

原文地址:https://www.cnblogs.com/163yun/p/9548123.html

时间: 2024-12-18 02:32:52

【专家坐堂】四种并发编程模型简介的相关文章

IO复用、多进程和多线程三种并发编程模型

I/O复用模型 I/O复用原理:让应用程序可以同时对多个I/O端口进行监控以判断其上的操作是否可以进行,达到时间复用的目的.在书上看到一个例子来解释I/O的原理,我觉得很形象,如果用监控来自10根不同地方的水管(I/O端口)是否有水流到达(即是否可读),那么需要10个人(即10个线程或10处代码)来做这件事.如果利用某种技术(比如摄像头)把这10根水管的状态情况统一传达到某一点,那么就只需要1个人在那个点进行监控就行了,而类似与select或epoll这样的多路I/O复用机制就好比是摄像头的功能

程序员必知的七种并发编程模型

1.线程与锁线程与锁模型有很多众所周知的不足,但仍是其他模型的技术基础,也是很多并发软件开发的首选. 2.函数式编程 函数式编程日渐重要的原因之一,是其对并发编程和并行编程提供了良好的支持.函数式编程消除了可变状态,所以从根本上是线程安全的,而且易于并行执行. 3.Clojure之道——分离标识与状态 编程语言Clojure是一种指令式编程和函数式编程的混搭方案,在两种编程方式上取得了微妙的平衡来发挥两者的优势. 4.actor actor模型是一种适用性很广的并发编程模型,适用于共享内存模型和

Java多线程-并发编程模型

以下内容转自http://ifeve.com/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B%E6%A8%A1%E5%9E%8B/: 并发系统可以采用多种并发编程模型来实现.并发模型指定了系统中的线程如何通过协作来完成分配给它们的作业.不同的并发模型采用不同的方式拆分作业,同时线程间的协作和交互方式也不相同.这篇并发模型教程将会较深入地介绍目前(2015年,本文撰写时间)比较流行的几种并发模型. 并发模型与分布式系统之间的相似性 本文所描述的并发模型类似于分布式系统中使

4.并发编程模型

并发系统可以采用多种并发编程模型来实现.并发模型指定了系统中的线程如何通过协作来完成分配给它们的作业.不同的并发模型采用不同的方式拆分作业,同时线程间的协作和交互方式也不相同.这篇并发模型教程将会较深入地介绍目前(2015年,本文撰写时间)比较流行的几种并发模型. 并发模型与分布式系统之间的相似性 本文所描述的并发模型类似于分布式系统中使用的很多体系结构.在并发系统中线程之间可以相互通信.在分布式系统中进程之间也可以相互通信(进程有可能在不同的机器中).线程和进程之间具有很多相似的特性.这也就是

ASP.NET MVC下的四种验证编程方式[续篇]

在<ASP.NET MVC下的四种验证编程方式>一文中我们介绍了ASP.NET MVC支持的四种服务端验证的编程方式("手工验证"."标注ValidationAttribute特性"."让数据类型实现IValidatableObject或者IDataErrorInfo"),那么在ASP.NET MVC框架内部是如何提供针对这四种不同编程方式的支持的呢?接下来我们就来聊聊这背后的故事. 一.ModelValidator与ModelVal

ASP.NET MVC下的四种验证编程方式

原文:ASP.NET MVC下的四种验证编程方式 ASP.NET MVC采用Model绑定为目标Action生成了相应的参数列表,但是在真正执行目标Action方法之前,还需要对绑定的参数实施验证以确保其有效性,我们将针对参数的验证成为Model绑定.总地来说,我们可以采用4种不同的编程模式来进行针对绑定参数的验证. 目录 一.手工验证绑定的参数 二.使用ValidationAttribute特性 三.让数据类型实现IValidatableObject接口 四.让数据类型实现IDataError

iOS中常用的四种数据持久化方法简介

iOS中常用的四种数据持久化方法简介 iOS中的数据持久化方式,基本上有以下四种:属性列表.对象归档.SQLite3和Core Data 1.属性列表涉及到的主要类:NSUserDefaults,一般 [NSUserDefaults standardUserDefaults]就够用了 @interface User : NSObject <NSCoding>@property (nonatomic, assign) NSInteger userID;@property (nonatomic,

基本的并发编程模型

基于进程的并发 基本模型 在TCP服务器编程中,多进程并发服务器通常由主进程负责连接的建立,然后fork出子进程,负责该连接剩下的行为,直到关闭. 关于多进程并发服务器有几点重要的内容: 通常服务器会运行很长时间,因此必须要包括一个SIGCHLD处理程序,来回收僵死子进程的资源.因为当SIGCHLD处理程序执行时,SIGCHLD信号是阻塞的,而Unix信号是不排队的,所以SIGCHLD处理程序必须准备好回收多个僵死子进程的资源. 父进程在fork调用后,将连接交给子进程处理,父子进程必须关闭他们

ASP.NET MVC下的四种验证编程方式【转】

ASP.NET MVC采用Model绑定为目标Action生成了相应的参数列表,但是在真正执行目标Action方法之前,还需要对绑定的参数实施验证以确保其有效 性,我们将针对参数的验证成为Model绑定.总地来说,我们可以采用4种不同的编程模式来进行针对绑定参数的验证. 目录 一.手工验证绑定的参数 二.使用ValidationAttribute特性 三.让数据类型实现IValidatableObject接口 四.让数据类型实现IDataErrorInfo接口 一.手工验证绑定的参数 在 定义具