Go 知识chan
- 1. 基本知识
- 1.1 定义
- 1.2 操作
- 1.3 操作限定
- 1.4 chan 读写
- 2. 原理
- 2.1 数据结构
- 2.2 环形队列
- 2.3 等待队列
- 2.4 类型消息
- 2.5 读写数据
- 2.6 关闭chan
- 3. 使用
- 3.1 操作符使用
- 3.2 select
- 3.3 for-range
https://a18792721831.github.io/
1. 基本知识
chan是go里面里面提供的语言层面的协程间的通信方式,用于并发通信。
1.1 定义
- 变量声明:
var ch chan int // 声明一个int型的chan
这种方式声明的chan,值为nil。并且每一种chan只能有一种类型。 - make声明:
ch1 := make(chan int)// 无缓存的chan
,ch2 := make(chan int, 4)// 有缓冲的chan
1.2 操作
操作符<-
表示数据流向,当chan在<-
左边,表示数据流向chan,也就是向chan写入数据;当chan在<-
右边,表示数据流出chan,
也就是从chan读取数据。
func TestChanOne(t *testing.T) {
var ch chan int
ch = make(chan int) // 无缓冲chan
go func() { // 创建一个协程,不断从chan中读取数据
<-ch
}()
ch <- 1 // 写入数据,因为没有缓冲,如果没有上面那句,这句会阻塞
var ch1 chan int
ch1 = make(chan int, 1) // 有缓冲chan
ch1 <- 2 // 因为有缓存,所以这句不会阻塞,但是因为缓冲长度是1,所以在写入就会阻塞
fmt.Printf("len=%d, cap=%d\n", len(ch1), cap(ch1))
fmt.Println(<-ch1) // 读取数据,因为有缓冲,并且有数据,所以不会阻塞
}
执行结果:
1.3 操作限定
默认的chan为双向可读写,chan在函数间传递的时候,可以使用操作符限制chan的读写。
func ChanRW(ch chan int) {
ch <- 1
<-ch
}
func ChanR(ch <-chan int) {
ch <- 1
<-ch
}
func ChanW(ch chan<- int) {
ch <- 1
<-ch
}
1.4 chan 读写
chan无缓冲时,从chan读取数据会阻塞,直到有协程向chan写入数据。向chan写入数据也会阻塞,直到有协程读取数据。
chan有缓冲时,从chan读取数据,如果缓冲区中没有数据,那么也会阻塞,直到有协程写入数据。向chan写入数据,如果缓冲区已经满了,也会阻塞,直到有协程从chan中读取数据。
对于nil的chan,不管是读还是写,都是永久阻塞。 – 这个一定注意。
使用内置函数close
可以关闭chan,尝试向关闭的chan写入数据会触发panic,但是仍然可以读取。
chan读取最多可以给两个变量赋值:
i := <- ch
j, ok := <- ch
第一个变量表示读取的值,第二个变量表示是否成功读取了数据。第二个变量不表示chan是否关闭。
对于关闭的有缓冲的chan存在两种情况:1. 缓冲区中没有数据; 2. 缓冲区中有数据。
对于有缓冲的chan并且用内置函数close
进行关闭,在继续读取,针对缓冲区中没有数据,读取的第一个值是该类型的零值,第二个值为false,表示没有成功读取数据。
如果缓冲区中存在数据,那么第一个值是缓冲区中的第一个数据,第二个值是true,表示正确读取到数据。
也就是说,只有缓冲区中没有数据了,第二个值才能代表chan已经关闭。
当chan没有缓冲区的时候,第二个值可以认为是chan的关闭状态。
2. 原理
2.1 数据结构
在源码包的src/runtime/chan.go
中定义了chan的结构:
type hchan struct {
qcount uint // 当前队列中剩余的元素个数
dataqsiz uint // 环形队列的长度,即可以存放的元素个数
buf unsafe.Pointer // 环形队列指针
elemsize uint16 // 每个元素的大小
closed uint32 // 关闭状态
elemtype *_type // 元素类型
sendx uint // 写入位置
recvx uint // 读取位置
recvq waitq // 阻塞读协程队列
sendq waitq // 阻塞写协程队列
lock mutex // 互斥锁
}
可以看出,一个chan基本上由数据缓冲,类型信息,和协程等待队列组成。
2.2 环形队列
环形队列主要是存储缓冲数据的,其实实现一个缓冲队列非常简单,只需要一个数组就能实现,每次数组指针到达最后一个元素,那么就重置为0即可。
var data []int
data = make([]int, 10)
for i := 0; i < 100; i++ {
data[i] = i
if i == len(data)-1 {
i = 0
}
}
上面的if就是实现了环形队列,所以名字叫做环形队列,实际上可能是线性的结构。
在chan中使用双指针实现读取和写入数据,读指针表示读取的元素位置,写指针表示写入的元素位置。
当读指针等于写指针的时候,表示数据读取完毕,整个队列为空。
当写指针等于读指针的时候,表示队列满,无法写入。
2.3 等待队列
因为同一个chan可以传给多个协程,就会出现多个协程等待读取的情况;对于写数据也是相同的,那么此时就会根据先后顺序,组成等待队列,等待的协程组成了读阻塞队列和写阻塞队列。
当对chan进行写操作,那么唤醒读队列的协程;当对chan进行读操作,那么唤醒写队列的协程。
对于chan进行写操作,如果读协程队列为空,那么写操作阻塞;对于chan进行读操作,如果写协程队列为空,那么读操作阻塞。
需要注意,上述情况还有一个附件条件:无缓冲chan,如果是有缓冲chan,那么对于写操作,缓冲区满,对于读操作,缓冲区空。
2.4 类型消息
在chan的定义中,保存了类型消息和类型长度,类型消息主要是传递过程中赋值,类型长度主要是计算缓冲区中元素的位置。
2.5 读写数据
对于chan读写数据,实际上就是数据进入缓冲区和读出缓冲区的操作。
在实现上则存在一定的技巧:
写入数据时,如果读协程等待队列不为空,那么直接将数据给读协程等待队列的第一个协程即可,不进行数据缓冲区的数据写入,然后在唤醒,最后读取的操作,避免了数据的额外处理。
同样的,读取数据时,需要甄别一下,因为chan本质上是队列,需要符合先入先出的规则,在写入数据时,因为chan为空,所以不管有没有缓冲区,都可以直接将数据交给等待的读取协程。
但是在读取数据的时候,如果存在缓冲区,那么就必须从缓冲区中读取,读取数据后,环形写入协程等待队列,将数据写入缓冲区的队尾。
只有无缓冲区的chan,才可以直接从写等待协程队列中唤醒协程,并将数据直接交给读取协程。
2.6 关闭chan
关闭chan的时候,会将读等待队列和写等待队列的全部协程都唤醒。
对于读等待的协程,赋值该类型的零值,并且将第二个值赋值为false。
需要注意,对于chan,不会同时既存在读等待队列协程,又存在写等待队列协程。(长时间同时,瞬时的同时可能存在)
对于写等待的协程,直接触发panic,向close的chan写入数据,直接panic。
除此之外,这些操作也会panic:
- 关闭nil的chan
- 关闭已经关闭的chan
- 向已经关闭的chan写入数据
3. 使用
3.1 操作符使用
使用<-
进行操作,同时也可以限制在当前函数范围内chan的操作方式,读还是写,还是读写。
3.2 select
对于chan的使用,不管是读取还是写入,非常导致阻塞,但是很多时候,我们又不希望整个程序阻塞,因为是并发的,可能当前chan没有数据,但是别的chan可能有数据,那么就可以在等待一个chan的时候,处理其他chan的数据,增加程序的性能。
使用select就可以实现监控多个chan。
sayOne := make(chan string)
sayTwo := make(chan string)
// 每两秒发送 one
go func() {
for {
sayOne <- "one"
time.Sleep(2 * time.Second)
}
}()
// 每两秒发送 two
go func() {
for {
sayTwo <- "tow"
time.Sleep(2 * time.Second)
}
}()
for {
// 如果 select 中的任何 case 都没有触发,那么 select 本身是会阻塞的,直到某个 case 触发
select {
case msg := <-sayOne:
fmt.Println(msg)
case msg := <-sayTwo:
fmt.Println(msg)
// 但是因为 select 触发的 case 是随机的,所以这里可能并不会正确执行第三个case
case <-time.After(10 * time.Second):
return
}
}
要想按照程序10秒后退出,可以使用定时器进行超时退出:
// 设置超时
time.AfterFunc(10*time.Second, func() {
os.Exit(0)
})
除此之外,还可以使用default
进行计时,然后进行超时退出操作。
sayOne := make(chan string)
sayTwo := make(chan string)
// 每两秒发送 one
go func() {
for {
sayOne <- "one"
time.Sleep(2 * time.Second)
}
}()
// 每两秒发送 two
go func() {
for {
sayTwo <- "tow"
time.Sleep(2 * time.Second)
}
}()
// 设置超时
//time.AfterFunc(10*time.Second, func() {
// os.Exit(0)
//})
tm := 0
for {
// 如果 select 中的任何 case 都没有触发,那么 select 本身是会阻塞的,直到某个 case 触发
select {
case msg := <-sayOne:
fmt.Println(msg)
case msg := <-sayTwo:
fmt.Println(msg)
// 但是因为 select 触发的 case 是随机的,所以这里可能并不会正确执行第三个case
//case <-time.After(10 * time.Second):
// return
default:
if tm == 10 {
return
}
time.Sleep(time.Second)
tm++
}
}
3.3 for-range
使用<-
操作符可以读取数据,除了使用<-
操作符,还可以使用for-range
进行数据的读取,就像读取一个数组一样。
ch := make(chan int, 10)
for i := 0; i < 10; i++ {
ch <- i
}
for i := range ch {
fmt.Println(i)
}
使用for-range
还有个好处,就是读取的数据都是成功的,不用去考虑数据是否读取成功,同时也不用考虑当chan关闭,当chan关闭后,for-range
也能正确的处理。
ch := make(chan int, 10)
for i := 0; i < 10; i++ {
ch <- i
}
// 5秒后关闭chan
time.AfterFunc(5*time.Second, func() {
close(ch)
})
// 每一秒读取一个元素
for i := range ch {
fmt.Println(i)
time.Sleep(time.Second)
}
即使第5秒的时候,chan被关闭了,也可以正确的读取全部的数据。