在Golang
的CSP模型
中,channel
在通信和同步中有着重要的作用。本文主要从基本概念、常见的用法、遇到的问题三个方面总结channel
的用法。
Golang Channel 是什么
channel
在中文交流中一般叫做通道
。
在CSP(Communicating Sequential Processes)并发模型中,核心的概念是两个并发实体通过channel
进行通信,Golang
中借用其中的一些概念,比如process
、channel
等。利用channel
可以让goroutine
之间通过通信来共享内存,我们可以将channel
看做内部的FIFO
队列。一些goroutine
将数据发送到队列,而其他goroutine
从队列中接收数据。
Golang Channel 的使用
Golang Channel 中的数据类型
在Go语言中,使用chan关键字创建一个通道,并且该通道只能传输相同类型的数据,不允许从同一通道传输不同类型的数据。
type T struct{}
var noBufferCh1 chan T
var noBufferCh2 = make(chan T)
var bufferCh = make(chan T, 10)
在Go语言中,通道工作有两个主要操作,一个正在发送,另一个正在接收。在通道中,发送和接收操作将阻塞,直到另一侧准备好为止。它允许goroutine
在没有显式锁或条件变量的情况下彼此同步。
Golang Channel 的操作
发送操作
发送操作用于在通道的帮助下将数据从一个goroutine
发送到另一个goroutine
。
像int
,float64
和bool
之类的值可以安全且容易地通过通道发送,因为它们是被复制的,因此不存在意外并发访问相同值的风险。同样,字符串也是安全的,因为它们是不可变的。但是对于通过通道发送指针或引用(例如slice,map等)并!!!不安全!!!,因为指针或引用的值可能会在发送到通道的同时或之后发生修改,甚至发送端goroutine或接收goroutine可能对其更改,并且结果和修改的时间不可预测。因此,在通道中使用指针或引用时,除非你明确知道会发生什么,否则必须确保它们一次只能由一个goroutine访问。
我曾经遇到过一次这样的情况,我维护了一个responseCh用来缓冲响应数据,但当responseCh消费比较慢时,某一条responseCh中的消息,被后续的逻辑修改。导致输出到终端的信息与我推到responseCh时不一致。示例代码如下,在一个复杂的系统中,很有可能就会因为疏忽导致这样的错误,需要自我检讨。
func main() {
type T struct{
Name string
}
ch := make(chan T, 1000)
go func() {
for{
select {
case msg := <- ch:
fmt.Println(msg)
}
}
}()
a := T{Name: "aaa"}
ch <- a
a.Name = "bbb"
select{}
}
接收操作
接收操作用于接收发送操作员发送的数据。
关闭通道
使用close
函数可以关闭通道。这是一个内置函数,并设置一个标志,指示不再有任何值将发送到该通道。向一个已关闭的通道发送数据,将会引发一个panic。
创建通道
每个通道值都有一个容量,容量为零的通道值称为无缓冲通道,容量为非零的通道值称为缓冲通道。
channel
的零值为nil
。
必须使用内置make
函数来创建非nil
通道值。
如下代码
//双向通道
var A = make(chan int)
//只入通道
var B = make(chan<- int)
//只出通道
var C = make(<-chan int)
//双向带缓冲区的通道
var A = make(chan int, 10)
make
函数调用的第二个参数指定新创建的通道的容量。第二个参数是可选的,其默认值为零。
- 如果某个通道的容量为零或不存在,则该通道将不进行缓冲,并且发送方将阻塞,直到接收方收到该值为止。
- 如果通道具有缓冲区,则发送方仅阻塞该值,直到将值复制到缓冲区;如果缓冲区已满,则意味着等待直到某些接收器检索到一个值。
- 接收器始终阻塞,直到有数据要接收为止。
- 从nil通道发送或接收将永远受阻。
Channel Select 操作
select允许同时等待多个通道操作,将goroutines和通道与select结合在一起,可以实现一些强大功能。select时会阻塞,直到其中一个case可以运行,然后执行那个case。如果有多个,它会随机选择一个,一定注意这里是随机选择一个case执行。类似的语法比如switch,但switch是有顺序的,这里要注意区别。
//
var msgIdCh = make(chan int64, 10)
var msgCh = make(chan string, 10)
select{
case content :=<-msgCh:
fmt.Println(content)
case id := <- msgCh:
fmt.Println(id)
}
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Down()
for {
msgIdCh <- time.Now().Unix()
}
}()
wg.Add(1)
go func() {
defer wg.Down()
for {
msgCh <- time.Now().String()
}
}()
wg.Wait()
总结
- 只使用有效的
channel
select-case
是随机的- 读写有先后,乱序会阻塞
- 函数间直接传递 channel,不需要传 channel 的指针,因为 chan 的变量本身就是一个指针