声明
本系列文章并不会停留在Go语言的语法层面,更关注语言特性、学习和使用中出现的问题以及引起的一些思考。
我们知道,Go实现了两种并发形式,第一种是多线程共享内存,其实就是Java,C++等语言的多线程并发,通过锁来进行访问。另一种则是Go特有的CSP(communicating sequential processes)并发模型。
什么是CSP?
CSP 是 Communicating Sequential Process 的简称,中文可以叫做通信顺序进程,是一种并发编程模型,由 Tony Hoare 于 1977 年提出。它是在串行时代提出的一个概念,慢慢的演化成了现在的一种并发模型。简单来说,CSP 模型由并发执行的实体(线程或者进程)所组成,实体之间通过发送消息进行通信,这里发送消息时使用的就是通道,或者叫 channel。那么,CSP 模型的关键是关注 channel,而不关注发送消息的实体。而Go 语言实现了 CSP 部分理论,具体的模式如下图所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8WYUV3fh-1586742080725)(/img/bVbFVHy)]
Channel 在 gouroutine 间架起了一条管道,在管道里传输数据,实现 gouroutine 间的通信;由于它是线程安全的,所以用起来非常方便;channel 还提供 “先进先出” 的特性;它还能影响 goroutine 的阻塞和唤醒。
说到这,可能就有的同学有些疑问,为什么要用channel,Goroutine不就可以看作一个线程,然后线程间通信用共享内存来通信不行么?请往下看。
为什么要用channel
相信大家都听过这么一句话,Do not communicate by sharing memory; instead, share memory by communicating(不要通过共享内存来通信,而要通过通信来实现内存共享),这两句话难道不是一个意思么?从本质上来看,计算机上线程和协程同步信息其实都是通过共享内存来进行的,因为无论是哪种通信模型,线程或者协程最终都会从内存中获取数据,所以更为准确的说法是为什么我们使用发送消息的方式来同步信息,而不是多个线程或者协程直接共享内存?
我们从使用场景分析一下,首先,前半句应该是指我们多应用于多线程通信的方式,一般线程同步在线程间交换的信息仅仅是控制信息,比如某个A线程释放了锁,B线程能获取到锁并开始运行,这个不涉及数据的交换。数据的交换主要还是通过共享内存(共享变量或者队列)来实现,为了保证数据的安全和正确性,共享内存就必需要加锁等线程同步机制。而线程同步使用起来特别麻烦,容易造成死锁,且过多的锁会造成线程的阻塞以及这个过程中上下文切换带来的额外开销。我们通常会因为在代码中加锁而感到烦恼。
下半句呢?我理解后半句是说的channel来共享内存,在Go的这种方式中,要传递某个数据给另一个goroutine(协程),可以把这个数据封装成一个对象,然后把这个对象的指针传入某个channel中,另外一个goroutine从这个channel中读出这个指针,并处理其指向的内存对象。channel本身保证来同一时间只有一个goroutine能访问channel的数据,就不用开发者去处理锁了。
我们根据他们的差异来总结一下:
- 首先,使用发送消息来同步信息相比于直接使用共享内存和互斥锁是一种更高级的抽象,使用更高级的抽象能够为我们在程序设计上提供更好的封装,让程序的逻辑更加清晰;
- 其次,消息发送在解耦方面与共享内存相比也有一定优势,我们可以将线程的职责分成生产者和消费者,并通过消息传递的方式将它们解耦,不需要再依赖共享内存;
- 最后,Go 语言选择消息发送的方式,通过保证同一时间只有一个活跃的线程能够访问数据,能够从设计上天然地避免线程竞争和数据