CSP 模型详解
CSP 是 Go 并发模型背后的理论基础之一。
全称:
Communicating Sequential Processes
通信顺序进程
由英国计算机科学家
Tony Hoare
于 1978 年提出。
CSP 到底在解决什么问题
先看传统并发模型。
多个线程共享数据:
Thread1 ─┐
│
Thread2 ─┼── Shared Memory
│
Thread3 ─┘
大家都能修改同一块内存。
于是必须:
Mutex
RWMutex
Semaphore
来保证安全。
这种模式叫:
Shared Memory Concurrency(共享内存并发)
CSP 的想法完全不同:
Process A ←→ Channel ←→ Process B
每个 Process(进程/协程)拥有自己的状态。
彼此不直接访问对方的数据。
通过消息通信协作。
一个现实例子
共享内存模式
公司里有一本账本:
余额 = 1000
所有员工都能直接改:
张三 +100
李四 -50
王五 +200
为了避免冲突:
先加锁
再修改
释放锁
CSP 模式
只有财务能改账本:
财务拥有账本
其他人:
发送申请
"帮我存100"
"帮我取50"
财务依次处理:
消息1
消息2
消息3
因此:
没有人直接碰账本
这就是 CSP 的核心思想。
Go 是怎么体现 CSP 的
Go 的两个核心组件:
go func() {}
创建执行单元。
ch := make(chan int)
创建通信通道。
例如:
ch := make(chan string)
go func() {
ch <- "hello"
}()
msg := <-ch
fmt.Println(msg)
这里:
Goroutine A
│
▼
Channel
│
▼
Goroutine B
A 和 B 不共享变量。
只交换消息。
这就是 CSP。
CSP 的核心原则
可以概括为三句话:
1. 数据有所有者
例如:
balance := 1000
只有某个 Goroutine 能修改。
2. 不直接访问别人的数据
不要:
other.balance += 100
而是:
depositCh <- 100
发送请求。
3. 用消息协作
不是:
锁
共享变量
竞争
而是:
消息
Channel
事件流
Go 为什么适合 CSP
因为 Channel 是语言级特性。
例如:
select {
case msg := <-ch1:
fmt.Println(msg)
case msg := <-ch2:
fmt.Println(msg)
}
天然支持:
-
消息传递
-
多路复用
-
超时控制
-
取消机制
这比很多语言依赖第三方库实现 Actor/CSP 更自然。
CSP 和 Actor 模型的区别
有时会延伸问。
Actor(如 Erlang、Akka):
Actor A
Actor B
Actor C
特点:
-
每个 Actor 有邮箱(Mailbox)
-
通过消息通信
-
Actor 地址可寻址
CSP:
Process A
│
Channel
│
Process B
特点:
-
Channel 是一等公民
-
进程通过 Channel 建立连接
-
通信路径显式
简单理解:
Actor:
找到人再发消息
CSP:
找到管道再通信
Go 真的完全遵循 CSP 吗
实际上:
不是。
Go 借鉴了 CSP,但不是纯 CSP。
因为 Go 同时支持:
sync.Mutex
sync.RWMutex
sync.Atomic
例如:
var mu sync.Mutex
这明显属于:
Shared Memory
而不是 CSP。
所以更准确地说:
Go 的并发设计理念受 CSP 影响,鼓励使用 Channel 和 Goroutine 进行通信,但在工程实践中同时提供共享内存和锁机制作为补充。
回答模板
如果官问:
什么是 CSP?Go 为什么说自己是 CSP 风格?
可以回答:
CSP(Communicating Sequential Processes)是一种并发模型,其核心思想是让并发执行单元通过消息通信协作,而不是直接共享数据。Go 的 Goroutine 对应 CSP 中的 Process,Channel 对应通信通道。Go 鼓励由一个 Goroutine 持有数据,其他 Goroutine 通过 Channel 发送消息进行交互,从而减少锁竞争和竞态问题。不过 Go 并不是纯 CSP,它也提供 Mutex、RWMutex 和 Atomic 等共享内存并发工具,因此更准确地说 Go 是一种受 CSP 思想影响的并发语言。