45 | CSP模型:Golang的主力队员

Golang是一门号称从语言层面支持并发的编程语言,支持并发是Golang一个非常重要的特性。在上一篇文章《44 | 协程:更轻量级的线程》中我们介绍过,Golang支持协程,协程可以类比Java中的线程,解决并发问题的难点就在于线程(协程)之间的协作。

那Golang是如何解决协作问题的呢?

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

什么是CSP模型

我们在《42 | Actor模型:面向对象原生的并发模型》中介绍了Actor模型,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中发送数据,会导致发送消息的协程阻塞。

第三个区别则是关于消息送达的。在《42 | Actor模型:面向对象原生的并发模型》这篇文章中,我们介绍过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并没有经过广泛的生产环境检验,所以并不建议你在生产环境中使用。

欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。

精选留言

  • Sunqc

    2019-06-11 19:30:14

    会反复看这些内容,每次看感觉都不一样,另外老师推荐的 并发编程的艺术,还有图解java多线程模式,结合这些书看加深理解。持续关注并发编程
    作者回复

    感谢感谢😄

    2019-06-11 22:27:23

  • zhangtnty

    2019-06-11 07:48:46

    王老师辛苦了👍
    作者回复

    感谢😄

    2019-06-11 09:20:41

  • QQ怪

    2019-06-11 08:49:31

    更本看不够啊,不想老师停更
    作者回复

    赶紧见好就收😂😂

    2019-06-11 22:31:43

  • walkingonair

    2019-06-11 08:10:39

    最后一篇了,结束打卡,内容很丰富,还要复习两遍
  • 叫我天才好了

    2019-09-06 21:35:58

    第一遍只能说让我理解了并发的处理,还是要多复习几遍。加油吧,不拼一下你永远不知道你的潜力有多大,感谢老师的分享。
    作者回复

    加油👍

    2019-09-07 05:25:14

  • 每天晒白牙

    2019-07-17 16:37:39

    追完了
  • 宁悦

    2020-03-28 13:18:14

    今天看完了整个系列,觉得老师讲的非常好,自己也在看图解java多线程,加深理解。感谢老师
    作者回复

    感谢支持😄

    2020-03-31 12:59:13

  • helloworld

    2019-09-25 09:32:35

    终于看完了。对Java领域的并发编程有了更深一层的认识。认真对待专栏的每一节课程,学原理,看示例程序,思考课后的问题。尤其是第一部分关于Java多线程编程相关的理论介绍是让我
    收获最大的。工作中对于Java中的各种队列了解不是很多,在这个专栏中学习了。至于专栏中关于并发设计模式和案例分析两部分的讲解,感觉了解即可。专栏虽然结束了,但是对于并发的
    学习没有结束,继续!
    作者回复

    👍

    2019-09-25 12:58:32

  • 知行合一

    2019-07-22 13:59:42

    订了十几个专栏,老师讲的是最通熟易懂的,虽然有些知识点由于自己不够熟悉还需要多看几遍,找相关的书籍多加研习。谢谢老师!真心希望以后还能学习老师的新专栏
    作者回复

    过奖啦😄现在就想休息😂

    2019-07-30 19:29:20

  • 2019-07-01 00:22:55

    现在golang也很火 golang和java 以后怎么选
    作者回复

    都可以选择的时候,更多看公司的技术路线,团队积累,实际上没那么纠结

    2019-08-01 19:01:52

  • 马哲富

    2019-06-21 16:25:11

    2个星期看完了,还得慢慢消化,谢谢王老师,老师辛苦了!
    作者回复

    客气啦😄看的够快的👍

    2019-06-21 19:56:35

  • 海涛

    2019-12-13 19:26:39

    学的比较晚也比较慢,准备开始刷第二遍
    作者回复

    👍

    2019-12-14 18:05:25

  • jery

    2019-08-01 10:51:16

    想问老师一个问题,问题描述:集群中有A、B两台机器,集群与另一系统C进行通信。
    系统C作为服务端最大并发数为50。如何保证集群中两台机器并
    发向C发起请求控制在50并发之内。
    我的思考:解决方法1:A、B两台机器平分50个并发。2.建一张表,实时监控正在执行任务任务的线程数,让A、B并发总数不大于50.
    作者回复

    ab每台机器最多开25个线程就可以了,不知道是否有必要那么精确

    2019-08-01 18:56:43

  • L

    2019-07-02 15:16:55

    花了一个多月追完了
  • J.M.Liu

    2019-06-12 10:54:04

    全程跟完的,飞机起飞关机前,打卡。感谢老师
    作者回复

    感谢支持😄

    2019-06-12 11:40:41

  • 一道阳光

    2019-06-11 20:54:50

    个人感觉把前三个模块重点学完,后面的框架、其他模型有空了解下,不是重点。对于框架部分,有阅读源码能力有必要深入研究。
  • 2021-10-20 11:47:22

    19年买的,21年才看,老师写的太好了,yyds
  • M

    2021-05-12 16:42:25

    最后两章有点懵逼,不了解golang。。。
  • 竹马彦四郎的好朋友影法師

    2021-02-18 15:56:05

    2021年原地过年,把这个专栏刷掉了~ 嘿嘿,喜欢,~其实都知道,但是不系统,谢谢王老师提纲挈领,吾辈获益良多!
    作者回复

    优秀

    2021-03-01 19:47:52

  • Geek_30c31e

    2020-12-27 22:46:56

    来的比较晚,哈哈哈,持续复习。