channel通道
golang的并发模型是序列通信处理CSP(communicating sequential process)——使用通信来共享内存 ,避免goroutine因竞争共享内存频繁加锁产生的性能问题。
基本数据结构
golang中提供了一个特殊的类型channel实现goroutine之间的通信。channel类似于队列,先进先出。channel数据结构源码在src/runtime/chan.go下。
chan 使用 hchan 表示,它的传参与赋值始终都是指针形式,每个 hchan 对象代表着一个 chan。
- hchan 中包含一个缓冲区 buf,它表示已经发送但是还未被接收的数据缓存。buf 的大小由创建 chan 时的参数来决定。qcount 表示当前缓冲区中有效数据的总量,dataqsiz 表示缓冲区的大小,对于无缓冲区通道而言 dataqsiz 的值为 0。如果 qcount 和 dataqsiz 的值相同,则表示缓冲区用完了。
- __buf __缓冲区表示的是一个环形队列 。其中 **sendx 表示下一个发送的地址,recvx **表示下一个接收的地址。
- elemtype 是channel的中存放的具体类型,每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。
- recvq 表示等待接收的 sudog 列表,一个接收语句执行时,如果缓冲区没有数据而且当前没有别的发送者在等待,那么执行者 goroutine 会被挂起,并且将对应的 sudog 对象放到 recvq 中。
- sendq 类似于 recvq,一个发送语句执行时,如果缓冲区已经满了,而且没有接收者在等待,那么执行者 goroutine 会被挂起,并且将对应的 sudog 放到 sendq 中。
- closed 表示通道是否已经被关闭,0 代表没有被关闭,非 0 值代表已经被关闭。
- lock 用于对 hchan 加锁
type hchan struct {
qcount uint // total data in the queue
dataqsiz uint // size of the circular queue
buf unsafe.Pointer // points to an array of dataqsiz elements
elemsize uint16
closed uint32
elemtype *_type // element type
sendx uint // send index
recvx uint // receive index
recvq waitq // list of recv waiters
sendq waitq // list of send waiters
// lock protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
lock mutex
}
创建一个channel
var name chan elemType //声明中需要指定channel的具体类型 eg. var c chan int 声明了一个int的channel
我们使用make()
方法创建,可以指定chan的缓存区大小。
c := make(chan int, 10) //make创建一个int类型的channel,容量大小为10
c1 := make(chan int) // 无缓存的channel
make()函数最后会调用底层的makechan()函数,返回一个通道的指针。当我们复制一个channel或用于函数参数传递时,我们只是拷贝了一个channel引用,因此调用者和被调用者将引用同一个channel对象。和其它的引用类型一样,channel的零值也是nil。
两个相同类型的channel可以使用==运算符比较。如果两个channel引用的是相同的对象,那么比较的结果为真。一个channel也可以和nil进行比较。
通道的操作
发送
ch := make(chan int,0) // 创建一个无缓存区的int channel
ch <- 998 // 向通道发送一个int类型的值10
接收
receiver := <-ch //将通道ch的值取出,赋值给变量receiver
<- ch //将通道ch的值取出,忽略结果
关闭
close(ch) // 关闭通道
关于关闭通道需要注意的事情是,只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。
关闭后的通道有以下特点:
- 对一个关闭的通道再发送值就会导致panic。
- 对一个关闭的通道进行接收会一直获取值直到通道为空。
- 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
- 关闭一个已经关闭的通道会导致panic。
channel的种类
不带缓存的channel
一个基于无缓存Channels的发送操作将导致发送者goroutine阻塞,直到另一个goroutine在相同的Channels上执行接收操作,当发送的值通过Channels成功传输之后,两个goroutine可以继续执行后面的语句。反之,如果接收操作先发生,那么接收者goroutine也将阻塞,直到有另一个goroutine在相同的Channels上执行发送操作。
基于无缓存Channels的发送和接收操作将导致两个goroutine做一次同步操作。 因为这个原因,无缓存Channels有时候也被称为同步Channels 。当通过一个无缓存Channels发送数据时,接收者收到数据发生在再次唤醒发送者goroutine之前。
单向channel
当一个channel作为一个函数参数时,它一般总是被专门用于只发送或者只接收。为了表明这种意图并防止被滥用,Go语言的类型系统提供了单方向的channel类型,分别用于只发送或只接收的channel。类型 chan<- int
表示一个只发送int的channel,只能发送不能接收。相反,类型<-chan int
表示一个只接收int的channel,只能接收不能发送。(箭头<-
和关键字chan的相对位置表明了channel的方向 。)这种限制将在编译期检测。
func f1(out chan<- int) {}
func f2(out chan<- int, in <-chan int){}
在调用f1或f2的时候,传入的chan类型参数会自动隐式转换为chan<- int
和<-chan int
类型,这种转换只是单向的,没有单向channel类型转换为chan类型。
因为关闭操作只用于断言不再向channel发送新的数据,所以只有在发送者所在的goroutine才会调用close函数,因此对一个只接收的channel调用close将是一个编译错误。
带缓存的channel
带缓存的Channel内部持有一个元素队列。队列的最大容量是在调用make函数创建channel时通过第二个参数指定的。通过内置函数cap()
可以获得channel的容量。对于内置的len函数,如果传入的是channel,那么将返回channel内部缓存队列中有效元素的个数。因为在并发程序中该信息会随着接收操作而失效,但是它对某些故障诊断和性能优化会有帮助。
package main
import "fmt"
func main() {
ch := make(chan int, 10) // 创建一个最大容量为10的channel
ch <- 233
ch <- 2
ch <- 3
fmt.Println(cap(ch)) // 10
fmt.Println(len(ch)) // 3
fmt.Println(<-ch) // 233
fmt.Println(len(ch)) // 2
}
向缓存Channel的发送操作就是向内部缓存队列的尾部插入元素,接收操作则是从队列的头部删除元素。如果内部缓存队列是满的,那么发送操作将阻塞直到因另一个goroutine执行接收操作而释放了新的队列空间。相反,如果channel是空的,接收操作将阻塞直到有另一个goroutine执行发送操作而向队列插入元素。