0%

CSP模型

Golang是从语言层面支持并发的编程语言。Golang支持协程,协程可以类比为Java中的线程,解决并发问题的难点在于线程之间的协作。

Golang提供了两种方案解决协作问题:

  • 支持协程之间以共享内存的方式通信,Golang提供了管程和原子类来对协程进行同步控制,这个方案与Java语言类似;
  • 支持协程之间以消息传递(Message-Passing)的方式通信,本质上是要避免共享,Golang的这个方案是基于CSP(Communicating Sequential Processes)模型实现的。

CSP模型定于

Actor模型中Actor之间是不嫩个共享内存的,彼此之间通信只能依靠消息传递的方式。Golang实现的CSP模型和Actor模型看上去非常相似,“不要以共享内存方式通信,要以通信方式共享内存(Don’t communicate by sharing memory, share momory by communicating)”。 Golang中协程之间,推荐以通信的方式共享内存,实际上指的就是协程之间以消息传递的方式来通信。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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
}

示例代码的目标是打印从1累加到100亿的结果,如果使用单个协程来计算,大概需要4秒多的时间。单个协程,只能用到CPU中的一个核,为了提高性能,可以用多个协程来并行计算,发挥多核的优势。

用4个子协程来并行执行,分别计算[1, 25亿]、(25亿, 50亿]、(50亿, 75亿]、(75亿, 100亿],最后再在主协程中汇总4个子协程的计算结果。主协程要汇总4个子协程的计算结果,势必要和4个子协程之间通信,Golang中协程之间通信推荐的是使用channel,可以理解为现实世界中的管道。calc()方法的返回值是一个只能接收数据的channel ch,它创建的子协程会把计算结果发送到这个ch中,而主协程也会将这个计算结果通过ch读取出来。

CSP模型与生产者-消费者模式

Golang实现的CSP模型可以类比为生产者-消费者模式,channel可以类比为生产者-消费者模式中的阻塞队列。
Golang中的channel的容量可以是0,容量为0的channel在Golang中称为无缓冲的channel,容量大于0的则称为有缓冲的channel。

无缓冲的channel类似于Java中提供的SynchronousQueue,主要用途是在两个协程之间做数据交换。(例如累加器示例中calc()方法内部创建的channel就是无缓冲的channel)

有缓冲的示例如下,创建一个容量为4的channel,同时创建4个协程作为生产者、4个协程作为消费者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//创建一个容量为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,主协程中只能通过它接收数据,而不能通过它发送数据,如果试图通过它发送数据,编译器会提示错误。

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模型中,是能保证消息百分百送达的。这种百分百送达的代价是可能会导致死锁。

    1
    2
    3
    4
    5
    6
    7
    8
    //创建一个无缓冲的channel-ch,然后从ch中接收数据,此时主协程阻塞
    //main()方法中的主协程阻塞,整个应用就阻塞了-死锁
    func main() {
    //创建一个无缓冲的channel
    ch := make(chan int)
    //主协程会阻塞在此处,发生死锁
    <- ch
    }

总结

Golang支持传统的共享内存的协程间通信方式,也支持使用CSP模型,以通信的方式共享内存。

Golang中实现的CSP模型,支持select语句-类似网络编程里的多路复用函数select(),只要有一个channel能够发送成功或者接收到数据就可以跳出阻塞状态。

Java领域可以借助第三方的类库JCSP来支持CSP模型。

CSP模型-托尼·霍尔(Tony Hoare)
霍尔管程模型-Hoare