应用场景
在生产者-消费者模型的应用场景下,很自然地会想到线程安全队列 !生产者把产品放到队尾而消费者从队首取出产品消费掉。事实上,人们甚至根据生产者一个还是多个,消费者有一个还是多个,排列组合一下,将线程安全队列分成很多种,然后根据有界(bounded)无界(unbounded)。
Go语言中的buffered channel也可以认为是一种有界多生产者多消费者线程安全队列(Bounded MPMC queue)。在线程并发场景下,buffered channel是bounded MPMC queue,是CSP的实践之一,与Monitor或semaphore有同等表达能力。
概念
CSP(communicating sequential processes)也叫通信顺序进程,是一种并发编程模型,是一个很强大的并发数据模型,是上个世纪七十年代提出的,用于描述两个独立的并发实体通过共享的通讯 channel(管道)进行通信的并发模型。相对于Actor模型,CSP中channel是第一类对象,它不关注发送消息的实体,而关注在发送消息时使用的channel。
严格来说,CSP 是一门形式语言(类似于 ℷ calculus),用于描述并发系统中的互动模式,也因此成为一众面向并发的编程语言的理论源头,并衍生出了 Occam/Limbo/Golang…
而具体到编程语言,如 Golang,其实只用到了 CSP 的很小一部分,即理论中的 Process/Channel(对应到语言中的 goroutine/channel):这两个并发原语之间没有从属关系,Process 可以订阅任意个 Channel,Channel 也并不关心是哪个 Process 使用它进行通信;Process 围绕 Channel 进行读写,形成一套有序阻塞和可预测的并发模型。
Go中的CSP模型
channel的概念
channel是用来传递数据的一个数据结构,是golang中的主要协程间通信模型,是CSP模型的核心实践。这意味着同步通常也是用channel做的,而golang通常也不建议使用mutex、condition_variable等经典的同步方式。
channel的实现
- 功能
channel可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。操作符
<-用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。如果通道接收不到数据后 ok 就为 false,这时通道就可以使用 close() 函数来关闭。 - 底层数据结构
type hchan struct { // chan 里元素数量 qcount uint // chan 底层循环数组的长度 dataqsiz uint // 指向底层循环数组的指针 // 只针对有缓冲的 channel buf unsafe.Pointer // chan 中元素大小 elemsize uint16 // chan 是否被关闭的标志 closed uint32 // chan 中元素类型 elemtype *_type // element type // 已发送元素在循环数组中的索引 sendx uint // send index // 已接收元素在循环数组中的索引 recvx uint // receive index // 等待接收的 goroutine 队列 recvq waitq // list of recv waiters // 等待发送的 goroutine 队列 sendq waitq // list of send waiters // 保护 hchan 中所有字段 lock mutex }buf指向底层循环数组,只有缓冲型的 channel 才有
sendx,recvx均指向底层循环数组,表示当前可以发送和接收的元素位置索引值(相对于底层数组)sendq,recvq分别表示被阻塞的 goroutine,这些 goroutine 由于尝试读取 channel 或向 channel 发送数据而被阻塞
waitq是sudog的一个双向链表,而sudog实际上是对 goroutine 的一个封装
lock用来保证每个读 channel 或写 channel 的操作都是原子的
多个通道
-
select的概念
模仿I/O多路复用机制,select可以在一个语句里使用多个 case 同时监听多个 channl 的读写状态。select语句是channel的重要特性, 其他支持channel的语言通常也有select类似物,比如clojure的alt!宏,rust的select!宏。而select语句因为要锁全部channel, 可能性能不高 -
select的实现
在解释为什么这么写之前,我们得先说明一下select是怎么work的,golang中,select可以分成以下几个主要步骤:- 执行到 select 时,会锁住所有的 channl 并且,打乱 case 结构体的顺序
- 按照打乱的顺序遍历,如果有就绪的信号,就直接走对应 case 的代码段,之后跳出 select
- 如果没有就绪的代码段,但是有 default 字段,那就走 default 的代码段,之后跳出 select
- 如果没有 default,那就将当前 goroutine 加入所有 channl 的对应等待队列
- 当某一个等待队列就绪时,再次锁住所有的 channl,遍历一遍,将所有等待队列中的 goroutine 取出,之后执行就绪的代码段,跳出select
-
case的实现
case 对应的 channel 都会被封装到一个结构体中,每一个 case 对应的数据结构如下:type scase struct { c *hchan // chan elem unsafe.Pointer // 读或者写的缓冲区地址 kind uint16 //case语句的类型,是default、传值写数据(channel <-) 还是 取值读数据(<- channel) pc uintptr // race pc (for race detector / msan) releasetime int64 }