一、多线程共享内存
1. 概念
多线程共享内存模型是一种并发编程模型,其中多个线程在同一个进程的地址空间中共享相同的内存区域 。这种模型允许多个线程并发地读取和写入相同的数据结构,但也引入了一些潜在的问题,其中最常见的问题之一就是竞态条件(Race Condition)。
竞态条件(Race Condition)是多线程或多进程并发执行时,由于执行顺序不确定 而导致程序的最终状态 依赖于不同执行序列的结果的情况。这意味着多个线程或进程在访问和修改共享数据时,如果没有适当的同步机制,可能导致意外的结果。
竞态条件的经典示例是两个线程同时对共享变量进行读取-修改-写入操作。如果没有适当的同步机制,两个线程可能同时读取相同的值,然后基于相同的值进行修改,最终写入回去。由于执行的顺序不确定,可能导致最终结果不是期望的结果。
多线程共享内存模型的关键概念:
共享内存: 多个线程共享同一块内存区域,这使得它们可以直接访问和修改相同的数据结构。
并发执行: 多个线程可以同时执行,每个线程都有自己的执行路径,但它们可以同时访问共享的数据。
同步机制: 由于多线程同时访问相同的数据可能导致竞态条件,开发者需要使用同步机制来确保对共享数据的安全访问。这包括使用锁(mutex)、信号量(semaphore)、条件变量等。
2. 问题
竞态条件说白了就是多线程并发访问同一内存空间 但是(竞态)缺少合适的同步机制导致的运行结果不确定。以下是竞态条件可能引发的一些典型问题:
-
临界区问题: 多个线程在同一时刻尝试进入或修改相同的临界区,导致数据不一致性。
-
数据竞争: 多个线程同时访问相同的内存位置,并且至少有一个线程进行写操作,可能导致未定义的行为或不稳定的结果。
-
死锁: 当线程相互等待对方释放资源,而无法继续执行时,就会发生死锁。死锁可能发生在多个线程试图获取相互依赖的锁时。
-
活锁: 类似于死锁,但线程不是被阻塞,而是一直在尝试解决冲突,导致系统无法取得进展。
-
饥饿: 某些线程可能由于竞争资源不公平而一直无法获得所需的资源,导致饥饿问题。
避免竞态条件的办法,让他同步确定的执行:
-
锁和同步机制: 使用互斥锁(mutex)、信号量等同步机制来确保在任意时刻只有一个线程能够访问共享数据。
-
原子操作: 使用原子操作来确保某些操作是不可分割的,从而避免在执行过程中被中断导致竞态条件。
-
数据不变性: 尽量使用不可变数据结构,避免在多线程环境中直接修改共享数据。
-
使用并发控制工具: 如读写锁、条件变量等,可以更细粒度地控制并发访问。
二、CSP并发编程模型
1.概念
CSP(Communicating Sequential Processes)是一种并发编程模型,最初由计算机科学家Tony Hoare在1978年提出。CSP的核心思想是通过在独立执行的进程之间进行通信来实现并发。
以下是CSP模型中的基本概念:
-
进程(Process): CSP中的进程是并发执行的基本单元。每个进程都有自己的私有状态和执行路径。
-
通信(Communication): 进程通过消息传递进行通信,而不是共享内存。这意味着一个进程可以向另一个进程发送消息,从而实现信息的传递和共享。
-
顺序执行(Sequential Execution): 即使在并发的环境中,每个进程仍然是按照指定的顺序执行的。这有助于避免一些常见的并发问题,如竞态条件和死锁。因为没有共享内存并且各进程按顺序执行不需要同步直接消除出现竞态条件的两个必要条件。
-
并行性(Concurrency): 进程可以同时执行,从而提高系统的整体性能。
2. Go 语言中应用
Go语言通过goroutine
和channel
实现了CSP模型。在Go中,goroutine
是轻量级的线程 ,而channel
是用于在goroutine
之间进行通信的数据结构。
以下是 Go 语言中CSP模型的实现:
package main
import (
"fmt"
"time"
)
func worker(id int, jobs <-chan int, results chan<- int) {
for job := range jobs {
fmt.Printf("Worker %d started job %d\n", id, job)
time.Sleep(time.Second) // 模拟耗时的工作
fmt.Printf("Worker %d finished job %d\n", id, job)
results <- job * 2
}
}
func main() {
const numJobs = 5
jobs := make(chan int, numJobs)
results := make(chan int, numJobs)
// 启动三个goroutine,模拟三个工作线程
for i := 1; i <= 3; i++ {
go worker(i, jobs, results)
}
// 发送五个工作任务
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs)
// 收集工作结果
for a := 1; a <= numJobs; a++ {
<-results
}
}
开启三个工作 worker 线程,通过从 jobs 通道中获取任务,模拟进行耗时的工作,运行完毕向 results通道中发送结果。保证了多线程同时安全执行工作。
三、Actor模型
Actor是一种并发计算模型,最初由Carl Hewitt于1973年提出。它提供了一种处理并发和分布式系统的抽象方法,通过将计算划分为独立的、自治的实体(称为Actor),这些实体通过消息传递进行通信。Actor模型的设计目标是简化并发编程,并提供一种避免共享状态和显式锁的方法。
以下是Actor模型的一些关键概念:
-
Actor: Actor是并发计算的基本单元。每个Actor都是一个独立的计算实体,有自己的状态、行为和邮箱。Actors之间通过消息进行通信。
-
消息传递: 在Actor模型中,通信是通过消息传递实现的。一个Actor可以向另一个Actor发送消息,触发接收者Actor的行为。
-
邮箱(Mailbox): 每个Actor都有一个邮箱,用于存储接收到的消息。当一个Actor收到消息时,它会根据消息的内容执行相应的行为。
-
地址: 每个Actor都有一个唯一的地址,用于标识它。其他Actor可以通过这个地址向目标Actor发送消息。
-
并发执行: Actor模型支持并发执行,因为每个Actor都是独立的计算实体,它们可以并发地执行,并通过消息传递进行协作。
-
没有共享状态: Actor之间没有共享的内存状态,通信是唯一的方式来传递信息。
以下是一个简单的Actor模型的示例,使用Akka库(一个实现了Actor模型的库,用于Scala和Java):
java
import akka.actor.ActorRef;
import akka.actor.ActorSystem;
import akka.actor.Props;
import akka.actor.UntypedActor;
// 定义一个简单的Actor
class MyActor extends UntypedActor {
@Override
public void onReceive(Object message) {
if (message instanceof String) {
String msg = (String) message;
System.out.println("Received message: " + msg);
}
}
}
public class ActorExample {
public static void main(String[] args) {
// 创建Actor系统
ActorSystem system = ActorSystem.create("MyActorSystem");
// 创建一个Actor
ActorRef myActor = system.actorOf(Props.create(MyActor.class), "myActor");
// 发送消息给Actor
myActor.tell("Hello, Actor!", ActorRef.noSender());
// 关闭Actor系统
system.terminate();
}
}
在这个例子中,定义了一个简单的Actor(MyActor
),并通过Akka库创建了一个Actor系统。然后,创建了一个Actor实例,并向它发送了一条消息。Actor接收到消息后,执行了相应的行为。
四、CSP和Actor区别
- 和Actor的直接通讯不同,CSP模式则是通过Channel进⾏通讯的,更松耦合⼀些。
- Go中channel是有容量限制并且独⽴于处理Groutine,⽽如Erlang,Actor模式中的mailbox容量是⽆限的,接收进程也总是被动地处理消息。
通信方式:
- CSP: CSP模型强调通过在进程之间进行同步通信来进行并发。通信是通过在通道上发送和接收消息来实现的,这些通道可以是同步或异步的。
- Actor模型: Actor模型也使用消息传递进行通信,但它不仅仅限制于同步通信。消息是通过异步方式发送给目标Actor的,因此发送消息的Actor不需要等待接收者处理消息。
独立性:
- CSP: CSP中的进程是相对独立的,它们通过通信进行协作,但通常是无状态的。
- Actor模型: Actor是具有状态的实体,它们封装了自己的状态和行为。每个Actor都是自治的,有自己的私有状态,可以独立地处理消息。
并发性:
- CSP: CSP模型通常涉及并发执行的进程,它们之间通过通信进行同步。
- Actor模型: Actor模型也涉及并发执行,但每个Actor是一个独立的计算实体,可以并发地执行,而不需要过多的同步。
灵活性:
- CSP: CSP更加关注同步和进程之间的协作,更适用于某些同步问题。
- Actor模型: Actor模型更注重封装和独立性,适用于构建具有私有状态和行为的独立实体。
错误处理:
- CSP: CSP通常使用一些错误处理机制,如超时或选择语句,来处理可能发生的并发问题。
- Actor模型: Actor模型中通常使用监督树等机制来处理错误,一个Actor可能监督其他Actor的行为,并在需要时进行适当的处理。
总体而言,虽然CSP和Actor模型都是用于并发编程的强大工具,但它们的设计目标和重点略有不同。CSP更强调同步和通信,而Actor模型更强调独立实体和异步消息传递。选择使用哪种模型通常取决于具体的应用需求和开发者的偏好。