文章目录
go channel 学习
channel特性
1、channel,可译为通道,是go语言协程goroutine之间的通信方式。
2、channel通信可以想象成从管道的一头塞进数据,从另一头读取数据。
3、协程通过channel通信可以不用进行加锁操作。
4、把数据发往无缓冲通道,如果接收方没有接收。发送操作将持续阻塞,此时会 释放cpu,执行其他协程,并且查看其他携程是否能够解除阻塞
5、接收将持续阻塞直到发送方发送数据
channel的创建和使用
c1 := make(chan int) // 无缓冲
c2 := make(chan interface{}) // 任意类型通道
c3 := make(chan int, 1) // 有缓冲
type Str struct{}
c4 := make(chan *Str) // 指针类型通道
c5 := make(chan struct{})
// 从channel变量c中读取数据,保存到变量v中
v := <-c
// 从channel变量c中读取数据,数据直接丢弃
<-c
接收通道数据(阻塞和非阻塞-select)
1、阻塞接收数据
data:= <- ch
2、非阻塞接收数据(select)
data, ok := <-ch
为了能知道当前 channel 是否被关闭,可以使用下面的写法来判断。
if v, ok := <-ch; !ok {
fmt.Println("channel 已关闭,读取不到数据")
}
还可以使用下面的写法不断的获取 channel 里的数据:
for data := range ch {
// get data dosomething
}
非阻塞可能会造成高cpu, 使用较少
实现接收超时检测, 可以使用 select 和 计时器channel
select {
case ack:= <- ch:
return ack, nil
case <- time.After(time.Second):
return "", errors.New("time out")
}
在写程序时,有时并不单单只会和一个 goroutine 通信,当我们要进行多 goroutine 通信时,则会使用 select 写法来管理多个 channel 的通信数据:
func Select() {
ch1 := make(chan struct{})
ch2 := make(chan struct{})
// ch1, ch2 发送数据
go func (){
for{
fmt.Println("send ch1")
ch1<- struct{}{}
ch2<- struct{}{}
time.Sleep(10*time.Second)
}
}()
go func (){
for {
fmt.Println("send ch2")
ch2<- struct{}{}
ch1<- struct{}{}
time.Sleep(10*time.Second)
}
}()
//延时等待数据先发送,在接收,模拟数据同时到达select的处理
time.Sleep(4*time.Second)
// channel 数据接受处理
for {
select {
case <-ch1:
fmt.Println("read- ch1")
case <-ch2:
fmt.Println("read ch2")
//default:
// fmt.Println("test default")
}
fmt.Println("continue -------")
}
}
------------------------------------------------------
执行结果如下:
send ch2
send ch1
read ch2
continue -------
read- ch1
continue -------
read ch2
continue -------
read- ch1
continue -------
注意:
- 图中select中的default 会导致select无阻塞,也会导致cpu飙高问题
- 去掉default,select会阻塞,直到通道有数据时解除
- 当两个通道同时有数据产生时,选择其中一个通道去执行,直到所有通道数据都处理完毕–
无缓冲channel
1、如果在一个协程里写这样的代码,一定会死锁:
func main(){
ch := make(chan int)
ch <- 1
<- ch
}
无缓冲的channel的读写者必须同时完成发送和接收,而不能串行,显然单协程无法满足。所以这里造成了循环等待,会死锁。
channel的deadlock
往 channel 里读写数据时是有可能被阻塞住的,一旦被阻塞,则需要其他的 goroutine 执行对应的读写操作,才能解除阻塞状态。
然而,阻塞后一直没能发生调度行为,没有可用的 goroutine 可执行,则会一直卡在这个地方,程序就失去执行意义了。此时 Go 就会报 deadlock 错误,如下代码:
读channel和写channel都需要出现,单独出现会死锁
func main(){
ch := make(chan int)
ch <- 1
}
func main(){
ch := make(chan int)
<-ch
}
带缓冲channel
阻塞条件:
- 通道被填满时,尝试再次发送数据发生阻塞
- 通道中没数据时,尝试接收数据时会发生阻塞
解释无缓冲channel和有缓冲channel的例子
- 无缓冲通道保证收发过程同步,类似于快递员给你打电话让你下楼取快递,递交快递的过程是同步的,你下楼取快递,过程结束
- 有缓冲通道类似于快递柜,快递员将快递放到快递柜中,通知你来取快递,快递员和用户成异步收发过程
关闭channel(有缓冲通道)
close(ch) //关闭通道
cap(ch) //计算通道的容量
- 给被关闭的通道发送数据将会触发panic
- 从已关闭的通道中接收数据时将不会发生阻塞
func main() {
ch := make(chan int, 2)
ch <- 0
ch <- 1
close(ch)
for i:=0;i<cap(ch)+1;i++ {
v, ok := <- ch
fmt.Println(v, ok)
}
}
-----------------
0 true
1 true
0 false
channel使用空结构体
特点:省内存,尤其在事件通信的时候,空结构体不占内存
channel := make(chan struct{})
go func() {
// ... do something
channel <- struct{}{}
}()
fmt.Println(<-channel)
channel作为形参的练习
// <-chan 出通道
func Comsume(channel <-chan int) {
for {
data := <-channel
if data == 0 {
break
}
fmt.Println(data)
time.Sleep(1 * time.Second)
}
}
// chan<- 进入通道
func Producer(channel chan<- int) {
for i := 1; i < 10; i++ {
fmt.Println("wait")
channel <- i
fmt.Println("start")
}
channel <- 0
}
func ChanTets() {
channel := make(chan int)
go Comsume(channel)
Producer(channel)
}
多生产、单消费模型
func ProducerTest(header string, channel chan<- string) {
for {
channel <- fmt.Sprintf("%s: %v", header, rand.Int31())
time.Sleep(1*time.Second)
}
}
func CustomerTest(channel <-chan string) {
for {
message := <-channel
fmt.Println(message)
}
}
func Pcmodel() {
channel := make(chan string)
go ProducerTest("cat", channel)
go ProducerTest("dog", channel)
CustomerTest(channel)
}
channel 的底层原理
channel 创建后返回了 hchan 结构体,现在我们来研究下这个结构体,它的主要字段如下:
type hchan struct {
qcount uint // channel 里的元素计数
dataqsiz uint // 可以缓冲的数量,如 ch := make(chan int, 10)。 此处的 10 即 dataqsiz
elemsize uint16 // 要发送或接收的数据类型大小
buf unsafe.Pointer // 当 channel 设置了缓冲数量时,该 buf 指向一个存储缓冲数据的区域,该区域是一个循环队列的数据结构
closed uint32 // 关闭状态
sendx uint // 当 channel 设置了缓冲数量时,数据区域即循环队列此时已发送数据的索引位置
recvx uint // 当 channel 设置了缓冲数量时,数据区域即循环队列此时已接收数据的索引位置
recvq waitq // 想读取数据但又被阻塞住的 goroutine 队列
sendq waitq // 想发送数据但又被阻塞住的 goroutine 队列
lock mutex //同步锁-互斥锁
...
}
recvq和sendq分别是接收(<-channel)或者发送(channel <- xxx)的goroutine抽象出来的结构体(sudog)的队列。是个双向链表
无缓冲 channel读写原理
channel 先写再读
在这里,我们暂时认为有 2 个 goroutine 在使用 channel 通信,按先写再读的顺序,则具体流程如下:
可以看到,由于 channel 是无缓冲的,所以 G1 暂时被挂在 sendq 队列里,然后 G1 调用了 gopark 休眠了起来。
接着,又有 goroutine 来 channel 读取数据了:
此时 G2 发现 sendq 等待队列里有 goroutine 存在,于是直接从 G1 copy 数据过来,并且会对 G1 设置 goready 函数,这样下次调度发生时, G1 就可以继续运行,并且会从等待队列里移除掉。
注意:缓存链表中以上每一步的操作,都是需要加锁操作的!
- 每一加粗样式步的操作的细节可以细化为:
- 第一,加锁
- 第二,把数据从goroutine中copy到“队列”中(或者从队列中copy到goroutine中)
- 第三,释放锁
channel 先读再写
G1 暂时被挂在了 recvq 队列,然后休眠起来。
G2 在写数据时,发现 recvq 队列有 goroutine 存在,于是直接将数据发送给 G1。同时设置 G1 goready 函数,等待下次调度运行。
有缓冲 channel 读写原理
channel 先写再读
这一次会优先判断缓冲数据区域是否已满,如果未满,则将数据保存在缓冲数据区域,即环形队列里。如果已满,则和之前的流程是一样的。
当 G2 要读取数据时,会优先从缓冲数据区域去读取,并且在读取完后,会检查 sendq 队列,如果 goroutine 有等待队列,则会将它上面的 data 补充到缓冲数据区域,并且也对其设置 goready 函数。
channel 先读后写
此种情况和无缓冲的先读再写是一样流程,此处不再重复说明。
写数据注意:
- 向一个nil channel发送数据,会调用gopark函数将当前goroutine挂起
- 向一个已经关闭的channel发送数据,直接会panic
- 如果channel的recvq当前队列中有被阻塞的接收者,则直接将数据发送给当前goroutine
- 当channel的缓冲区还有空闲空间,则将数据发送到sendx指向缓冲区的位置
- 当没有缓冲区或者缓冲区满了,则会创建一个sudog的结构体将其放到channel的sendq队列当中陷入休眠等待被唤醒
读数据注意:
- 从一个nil channel接收数据,会调用gopark函数将当前goroutine挂起,让出处理器的使用权
- 从一个已经关闭并且缓冲区中没有元素的channel中接收数据,则会接收到该类型的默认元素,并且第二个返回值返回false
- 如果channel没有缓冲区且sendq的队列有阻塞的goroutine,则把sendq队列头的sudog中保存的元素值copy到目标地址中
- 如果channel有缓冲区且缓冲区里面有元素,则把recvx指向缓冲区的元素值copy到目标地址当中,sendq队列头的sudog的元素值copy到recvx指向缓冲区位置的地址当中
- 当上面的条件都不符合时,则会创建一个sudog的结构体将其放到channel的recvq队列当中陷入休眠等待被唤醒