Day864.CSP模型 -Java 并发编程实战
Posted 阿昌喜欢吃黄桃
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Day864.CSP模型 -Java 并发编程实战相关的知识,希望对你有一定的参考价值。
CSP模型
Hi,我是阿昌
,今天学习记录的是关于CSP模型
的内容。
CSP模型,是通过以消息传递
(Message-Passing)的方式多线程通信,以通信方式共享内存
Golang
是一门号称从语言层面
支持并发
的编程语言,支持并发是 Golang 一个非常重要的特性。在协程,Golang 支持协程,协程可以类比 Java 中的线程,解决并发问题的难点就在于线程(协程)之间的协作
。那 Golang 是如何解决协作问题的呢?
总的来说,Golang 提供了两种不同的方案:
- 一种方案支持协程之间以
共享内存
的方式通信,Golang 提供了管程
和原子类
来对协程进行同步控制,这个方案与 Java 语言类似; - 另一种方案支持协程之间以
消息传递
(Message-Passing)的方式通信,本质上是要避免共享
,Golang 的这个方案是基于CSP
(Communicating Sequential Processes)模型实现的。
Golang 比较推荐的方案是后者。
一、什么是 CSP 模型
Actor 模型 中 Actor 之间就是不能共享内存的,彼此之间通信只能依靠消息传递
的方式。
Golang 实现的 CSP 模型和 Actor 模型看上去非常相似,Golang 程序员中有句格言:“不要以共享内存方式通信,要以通信方式共享内存(Don’t communicate by sharing memory, share memory by communicating)。”
虽然 Golang 中协程之间,也能够以共享内存的方式通信,但是并不推荐;而推荐的以通信的方式共享内存
,实际上指的就是协程之间以消息传递方式来通信
。
下面一个简单的示例,看看 Golang 中协程之间是如何以消息传递的方式实现通信的。
示例的目标是打印从 1 累加到 100 亿的结果,如果使用单个协程来计算,大概需要 4 秒多的时间。
单个协程,只能用到 CPU 中的一个核,为了提高计算性能,可以用多个协程来并行计算,这样就能发挥多核的优势了。
在下面的示例代码中,用了 4 个子协程来并行执行,这 4 个子协程分别计算[1, 25 亿]、(25 亿, 50 亿]、(50 亿, 75 亿]、(75 亿, 100 亿],最后再在主协程中汇总 4 个子协程的计算结果。
主协程要汇总 4 个子协程的计算结果,势必要和 4 个子协程之间通信,Golang 中协程之间通信推荐的是使用 channel
,channel 你可以形象地理解为现实世界里的管道
。
另外,calc() 方法
的返回值是一个只能接收数据的 channel ch
,它创建的子协程会把计算结果发送到这个 ch 中,而主协程也会将这个计算结果通过 ch 读取出来。
import (
"fmt"
"time"
)
func main()
// 变量声明
var result, i uint64
// 单个协程执行累加操作
start := time.Now()
for i = 1; i <= 10000000000; i++
result += i
// 统计计算耗时
elapsed := time.Since(start)
fmt.Printf("执行消耗的时间为:", elapsed)
fmt.Println(", result:", result)
// 4个协程共同执行累加操作
start = time.Now()
ch1 := calc(1, 2500000000)
ch2 := calc(2500000001, 5000000000)
ch3 := calc(5000000001, 7500000000)
ch4 := calc(7500000001, 10000000000)
// 汇总4个协程的累加结果
result = <-ch1 + <-ch2 + <-ch3 + <-ch4
// 统计计算耗时
elapsed = time.Since(start)
fmt.Printf("执行消耗的时间为:", elapsed)
fmt.Println(", result:", result)
// 在协程中异步执行累加操作,累加结果通过channel传递
func calc(from uint64, to uint64) <-chan uint64
// channel用于协程间的通信
ch := make(chan uint64)
// 在协程中执行累加操作
go func()
result := from
for i := from + 1; i <= to; i++
result += i
// 将结果写入channel
ch <- result
()
// 返回结果是用于通信的channel
return ch
二、CSP 模型与生产者 - 消费者模式
可以简单地把 Golang 实现的 CSP 模型类比为生产者 - 消费者模式,而 channel 可以类比为生产者 - 消费者模式中的阻塞队列。
不过,需要注意的是 Golang 中 channel 的容量可以是 0,容量为 0 的 channel 在 Golang 中被称为无缓冲的 channel
,容量大于 0 的则被称为有缓冲的 channel
。
无缓冲的 channel 类似于 Java 中提供的 SynchronousQueue
,主要用途是在两个协程之间做数据交换
。
比如上面累加器的示例代码中,calc() 方法内部创建的 channel 就是无缓冲的 channel。而创建一个有缓冲的 channel 也很简单,在下面的示例代码中,创建了一个容量为 4 的 channel,同时创建了 4 个协程作为生产者、4 个协程作为消费者。
// 创建一个容量为4的channel
ch := make(chan int, 4)
// 创建4个协程,作为生产者
for i := 0; i < 4; i++
go func()
ch <- 7
()
// 创建4个协程,作为消费者
for i := 0; i < 4; i++
go func()
o := <-ch
fmt.Println("received:", o)
()
Golang 中的 channel 是语言层面支持的,所以可以使用一个左向箭头
(<-)来完成向 channel 发送数据和读取数据的任务,使用上还是比较简单的。
Golang 中的 channel 是支持双向传输的
,所谓双向传输,指的是一个协程既可以通过它发送数据,也可以通过它接收数据。
不仅如此,Golang 中还可以将一个双向的 channel 变成一个单向的 channel,在累加器的例子中,calc() 方法中创建了一个双向 channel,但是返回的就是一个只能接收数据的单向 channel,所以主协程中只能通过它接收数据,而不能通过它发送数据,如果试图通过它发送数据,编译器会提示错误。
对比之下,双向变单向的功能,如果以 SDK 方式实现,还是很困难的。
三、CSP 模型与 Actor 模型的区别
同样是以消息传递的方式来避免共享,那 Golang 实现的 CSP 模型和 Actor 模型有什么区别呢?
-
第一个最明显的区别就是:
Actor 模型中没有 channel
。虽然 Actor 模型中的 mailbox 和 channel 非常像,看上去都像个 FIFO 队列,但是区别还是很大的。Actor 模型中的 mailbox 对于程序员来说是“透明”的,mailbox 明确归属于一个特定的 Actor,是 Actor 模型中的内部机制;而且 Actor 之间是可以直接通信的,不需要通信中介。但 CSP 模型中的 channel 就不一样了,它对于程序员来说是“可见”的,是通信的中介,传递的消息都是直接发送到 channel 中的。 -
第二个区别是:
Actor 模型中发送消息是非阻塞的
,而CSP 模型中是阻塞的
。Golang 实现的 CSP 模型,channel 是一个阻塞队列,当阻塞队列已满的时候,向 channel 中发送数据,会导致发送消息的协程阻塞。 -
第三个区别则是关于消息送达的。 Actor 模型理论上
不保证消息百分百送达
,而在 Golang 实现的 CSP 模型中,是能保证消息百分百送达的。不过这种百分百送达也是有代价的,那就是有可能会导致死锁。比如,下面这段代码就存在死锁问题,在主协程中,创建了一个无缓冲的 channel ch,然后从 ch 中接收数据,此时主协程阻塞,main() 方法中的主协程阻塞,整个应用就阻塞了。这就是 Golang 中最简单的一种死锁。func main() // 创建一个无缓冲的channel ch := make(chan int) // 主协程会阻塞在此处,发生死锁 <- ch
四、总结
Golang 中虽然也支持传统的共享内存的协程间通信方式,但是推荐的还是使用 CSP 模型
,以通信的方式共享内存
。
Golang 中实现的 CSP 模型功能上还是很丰富的,例如支持 select 语句,select 语句类似于网络编程里的多路复用
函数 select()
,只要有一个 channel 能够发送成功或者接收到数据就可以跳出阻塞状态。
CSP 模型是托尼·霍尔
(Tony Hoare)在 1978
年提出的,不过这个模型这些年一直都在发展,其理论远比 Golang 的实现复杂得多,如果感兴趣,可以参考霍尔写的Communicating Sequential Processes这本电子书。
另外,霍尔在并发领域还有一项重要成就,那就是提出了霍尔管程模型
,这个应该很熟悉了,Java 领域解决并发问题的理论基础就是它。Java 领域可以借助第三方的类库JCSP来支持 CSP 模型,相比 Golang 的实现,JCSP 更接近理论模型,如果感兴趣,可以下载学习。
不过需要注意的是,JCSP 并没有经过广泛的生产环境检验,所以并不建议在生产环境中使用。
以上是关于Day864.CSP模型 -Java 并发编程实战的主要内容,如果未能解决你的问题,请参考以下文章