并发计算模型是理解和构建并发系统(如多线程程序、分布式系统)的理论和抽象框架。
1. 线程与锁模型
这是最传统、最接近底层(操作系统)的模型,也是大多数程序员最先接触的模型。
核心思想:并发的基本单位是线程。多个线程共享同一进程的内存空间(共享内存)。通过锁(如互斥锁Mutex、信号量Semaphore)等同步原语来协调线程对共享资源的访问,防止竞争条件。
优点:
直观:映射到操作系统的实现方式。
控制力强:细粒度的锁可以提供高性能。
缺点:
极易出错:容易产生死锁、活锁、优先级反转、数据竞争等问题。
难以测试和调试:并发问题常常难以复现。
可伸缩性差:在高度并发时,锁会成为性能瓶颈。
典型代表:Pthreads(POSIX线程), Java
synchronized关键字, C++std::thread和std::mutex。
2. actor模型
一种基于消息传递的更高层次的抽象,由Carl Hewitt在1973年提出,非常适合分布式系统。
核心思想:
Actor是并发计算的基本单元。每个Actor是一个轻量级的实体。
封装状态:每个Actor有自己的私有状态,并且不与其他Actor共享内存。状态变更只能由自己进行。
异步消息传递:Actor之间只能通过发送异步消息进行通信。
邮箱:每个Actor有一个邮箱(消息队列),用于接收来自其他Actor的消息。
处理行为:Actor顺序地处理其邮箱中的消息。处理消息时,它可以:
改变其内部状态。
发送消息给其他Actor。
创建新的Actor。
优点:
强隔离性:不存在共享内存,从根本上避免了数据竞争和锁的问题。
位置透明性:Actor可以在本地,也可以在远程机器上,通信机制在理想情况下是一致的。
容错性好:容易实现“监督树”等错误恢复机制。
缺点:
消息传递有性能开销(但避免了锁竞争)。
业务逻辑可能变得“碎片化”,分布在多个Actor的消息处理中。
典型代表:Erlang(最著名的实现), Akka(Java/Scala框架), Orleans (.NET)。
3. 通信顺序进程模型
由Tony Hoare提出的理论模型(CSP, Communicating Sequential Processes),与Actor模型类似,也基于消息传递,但有一个关键区别。
核心思想:
CSP的并发实体(进程)是匿名的,它们不直接持有对方的引用。
通信通过通道(Channel) 进行。进程必须通过共享的通道来发送和接收消息。
同步是核心:通信本身是同步的(** rendezvous, 会合**)。即,发送方和接收方必须同时准备好(在通道两端“会合”),消息才会被传递。否则,准备好的一方会被阻塞。
(注:许多现代实现也提供了带缓冲的异步通道作为选项)
与Actor模型的区别:
优点:与Actor类似,避免了共享内存的陷阱。同步通信使数据流更加明确。
典型代表:Go 语言(Goroutine + Channel是其核心并发原语), Occam 语言。
4. 数据并行模型
专注于同时对集合中的不同元素执行相同的操作。
核心思想:将大规模数据分成若干块,然后将这些块分配给多个处理单元(CPU核心、机器)并行处理。
优点:
抽象层次高:程序员只需指定“做什么”(对哪些数据执行什么操作),而不必关心“怎么做”(线程、锁、任务分配)。
非常高效:特别适合科学计算、图形处理、大数据分析等计算密集型任务。
缺点:适用场景有限,主要针对可以高度并行化的、计算模式统一的任务。
典型代表:MapReduce(Hadoop), CUDA(NVIDIA GPU编程), OpenMP(C++/Fortran等语言的指令集), Apache Spark。
5. 函数式并发
基于函数式编程的不可变性和无副作用特性。
核心思想:避免状态可变和共享。数据是不可变的(Immutable),函数是“纯函数”(输出只取决于输入,不产生副作用)。既然没有可变状态需要修改,自然也就不需要锁。
实现方式:并发通常通过 Future/Promise 或惰性计算来实现。Future 代表一个尚未完成的计算结果,可以异步获取。
优点:安全。极大地降低了理解和推理并发程序的难度。
缺点:需要改变编程思维模式;并非所有问题都适合用函数式表达;有时需要拷贝数据可能带来性能开销。
典型代表:Clojure(默认不可变数据结构), Scala(与Akka结合使用Future), Haskell(纯函数式语言)。