概述
并发经常和并行一起被提及,但是我们应该清楚“并发”与“并行”不同
• 并发:同时处理多个事物(逻辑级别)
• 并行性:同时做(执行)多件事(物理层面)
并发可以构造一种可以并行化的解决问题的方法,从而将只能串行处理的事务并行化,更好地利用当前多核CPU和分布式集群的能力。
然而并发编程与人们正常的思维方式不同,因此有各种编程模型的抽象来帮助我们以更方便、更不易出错的方式构建并发程序。 下面将简单介绍一些常见的并发编程模型,希望能够帮助大家对并发编程产生更多的兴趣。 这些模型各有优势,需要根据应用场景进行选择,而选择的前提是能够深入了解它们。
多线程编程模型
多线程模型是最常见的处理并发的方法,广泛应用于C/C++/JAVA等语言中。 主要特点是:
l 多个独立的执行流程。
l 共享内存(状态)。
l 抢占式调度。
l 依赖于锁、信号量等同步机制
多线程程序很容易编写(因为它们是顺序程序),但难以分析、难以调试,而且更容易出错。 常见的包括竞争条件、死锁、活锁、资源耗尽、优先级反转……等等。
为了降低多线程模型的编写难度,很多语言都在不断引入并发编程的新特性,比如Java。 类从 1996 年最早的 JDK 1.0 版本就已经存在,建立了最基本的线程模型。 这比直接调用POSIX接口构建多线程应用程序有了很大的改进。 然后在JDK5中,java.util. 引入了包,包括线程池(Pool)等类库,提高了Java并发编程的易用性。
到了JDK7,引入了Fork/Join框架,虽然底层仍然基于线程池实现。 不过,在编写并发逻辑时,它比传统的多线程方法更直观。 开发人员可以将一项大型作业抽象为多个并发的子任务,并将结果进行整合; 并且每个子任务都可以按照这个逻辑继续划分,充分发挥现代多核CPU的威力。
同时,Fork/Join框架还内置了Work-task调度机制,可以尝试自动平衡工作线程之间的任务负载,同时最大限度地减少线程竞争。 如下所示:
Ø 4个线程各自拥有独立的工作队列,避免单个任务队列竞争。
Ø 队列中的任务以类似后进先出的方式进入和退出。 由于整个作业是通过从一个大任务中fork出多个子任务来抽象出来的,因此可以认为粒度较大的任务会沉到队列底部。
Ø 当一个线程(本例中为线程D)的工作队列为空时,该线程会自动尝试从另一个线程(本例中为线程A)的队列底部“窃取”一个任务来执行。 既然是从底层窃取的任务,那么可以假设这个任务会扩展出更多的子任务,从而减少窃取行为的发生,降低线程争用的频率。
通过这些手段,Fork/Join框架可以帮助开发人员在执行并发任务时无需考虑手动实现高效的同步逻辑。
随后,JDK8中引入了并行流( )的概念。 该功能基于 Fork/Join 框架,但在易用性方面不断改进。 并行流采用了共享线程池的思想,这甚至为开发者简化了线程/线程池的配置逻辑。 当然,正是因为这个共享池(())是由JVM管理的,并且被JVM中的所有线程共享的,所以也导致了一些隐患。 如果开发者不了解并行流的底层实现机制,可能会导致应用程序出现问题。 使用并行流的任务被停止。 例如下面的代码示例:
由于 WS.url(url).get() 会触发 HTTP 请求,因此当执行此代码时,线程池将在 IO 操作上被阻塞。 这样一来,当前JVM中并行流中的所有任务都会被阻塞。
编程模型
“回调”是一个很容易理解的术语。 简单来说:一个函数(A)可以接受另一个函数(B)作为参数。 当执行过程到达某个点时,函数B作为参数就会被函数A调用并执行。这种行为称为回调。
实际上,回调通常用于异步事件。 也就是说,如果函数B没有被调用,函数A一般会先返回,然后当异步事件发生时,触发对函数B的调用。
然而,误用回调嵌套会导致著名的“地狱”问题,使代码难以阅读和维护。 例如以下代码片段:
为了避免出现这样的大坑,我们可以参考以下几类解决方案:
l /A+规范:是管理异步回调的代码结构和流程,是回调的语法糖。 可以将原来的嵌套回调函数扁平化,让代码逻辑更加清晰。 例如片段:
l:生成器/半协程模式:可以暂停函数的执行,保存上下文,并将控制权返回给调用者; 当再次调用时,可以恢复暂停状态并继续执行。 因此,该函数的行为与迭代器的行为非常相似。 每次触发都可以获得一个新的结果,而不是传统函数执行完之后一次性返回一系列值。 代码段:
l Async/Await:可以看作是方法的语法糖,可以更好的展现异步调用的语义:async关键字用于表明函数中有异步操作; await 关键字表示需要等待(异步)后续表达式。 结果。
Actor编程模型
Actor模型最早由Carl于1973年定义,后来由OTP(Open)推广。 Actor属于并发组件模型。 它通过组件定义了并发编程范式的高级阶段,避免用户直接接触多线程并发或线程池等基本概念。 它的消息传递更符合面向对象的初衷。
传统上,大多数流行语言中的并发基于多个线程之间的共享内存,使用同步机制来防止写入争用。 利用消息模型,每个人同时最多处理一条消息,并且可以向其他人发送消息,保证了独立写入的原则,从而巧妙地避免了多线程的写竞争。
Actor模型不仅对于并发单机应用程序的开发有意义,它也是可以用于分布式应用程序开发的场景:节点之间相互独立,只能依靠消息通信,且特性比如异步消息避免节点瓶颈就非常适合。 兼容Actor模型的使用。
Actor模型的特点是:
l 万物皆演员
l Actor是完全独立的,只允许消息传递,不允许其他“任何”共享。
l 每个Actor同时最多只能执行一项工作
l 每个Actor都有一个专属名字(非匿名)
l 消息传递完全异步;
l 消息是不可变的
在Java中,Akka可以用于Actor编程模型的应用程序开发。 Akka 将自己定义为一组工具包和运行时环境,用于在 JVM 上构建高并发、容错、分布式、消息驱动的应用程序开发。 详细介绍可以参见官网:akka.io/。
下面的代码片段展示了基于AKKA的开发示例:
我们定义两个 Actor: 和 。
我将处理几条消息
n 启动消息(该方法的调用可以看作是接收到独占启动事件的处理):主动发送Msg.GREET消息给(可以看作是对应的Actor独占)
n Msg.Done消息:收到消息后,停止当前Actor
n 其他消息:call()处理
l 将处理这些消息:
n Msg.GREET消息:输出一个字符串到.out,并用Msg.Done消息回复消息的发送者
n 其他消息:call()处理
,可以根据需要实例化并在多个线程中执行。 在编码过程中,不需要考虑传统多线程中的Lock/Wait/等同步方法,让两个Actor互相指示对方完成相应的动作。
CSP编程模型
CSP ( ) 最初由 Tony Hoare 在他 1978 年的论文中提出。 它是一种处理并发编程的设计模式或模型,指导并发程序的设计,为并发程序提供实用的组织方法或设计范式。 通过这种方法,可以减少并发程序引入的其他缺点,减少和避免并发程序的常见缺点和bug,并且可以用数学理论来证明。
CSP将程序分为两类模块,并且: 表示执行任务的顺序单元。 它们内部不存在并发,而是代表并发流之间的信息交互,例如共享数据的交换、修改、消息传递等。
另外,它们之间没有任何联系,使并发同步效果减少到一处,从而使问题受到约束和集中。 同步操作和争用并没有消失,只是受到了关注。 之间的合作提供原语支持,例如等等。
CSP的优点是让系统更加清晰,解耦,职责非常明确,易于理解和维护。
l 工人之间不直接沟通
l 将自己的消息(事件)发布到不同的渠道。 其他工作人员可以监听这些通道上的消息,并且发送者不知道谁在执行它们(匿名)
l 消息交互是同步方式
CSP模型的Java实现库是JCSP。 同时JDK中的和CSP中的类似。 它用在 .() 中。 任务提交者并不知道哪个底层线程将处理提交的任务,当提交任务操作完成时,一定有一个线程已经接受了该任务(并不意味着该线程已经开始执行)。 因此,提交操作的消息交互是同步的。 这和.()之类创建的线程池完全不同。 当其他线程池中完成提交操作后,给线程分配任务的动作是异步的。
另外,Go语言的内置&并发模型参考了CSP的思想。 因此,Go的并发编程强调不要使用共享内存进行线程通信,而应该依靠通信来共享数据(不要by ;, share by ),尽量避免Lock和线程争用。
参考
l ~/cgi-bin//.pdf
我/维基/
l /2012/waza.slide#1
我/维基//A
l www.cs.kent.ac.uk//ofa/jcsp/-jcsp.pdf
l /java-/index.html
升/698
关于我们:
网易书迷产品限时开放试用,零成本立即体验!
我们帮助各行业客户进行数字化转型升级,成功实现业务增长。 点击查看部分案例,解锁企业转型新思路: