文章目录
- channel
- 1.channe的数据结构
- 2.channel的操作
- 1.channel写入
- 2.channel读取
- selelect
- 面试题
- 1.channel是线程安全的吗?为什么?
- 2.channel的底层实现原理
- 3.对channel进行读写关闭操作?
- 算法题
- 1.select中case的使用
- 2.有4个goroutine,按编号1、2、3、4循环打印
channel
1.channe的数据结构
type hchan struct {qcount uint // total data in the queuedataqsiz uint // size of the circular queuebuf unsafe.Pointer // points to an array of dataqsiz elementselemsize uint16closed uint32timer *timer // timer feeding this chanelemtype *_type // element typesendx uint // 开始写的位置recvx uint // 开始读的位置recvq waitq // 添加读的等待队列sendq waitq // 添加写的等待队列lock mutex // runtime.Mutex 保证线程安全
}type waitq struct {first *sudoglast *sudog
}
对于channel我们可以缓存数据到里面,是因为有一个buf的数组用来缓冲数据,又因为channel可以同时提供读写功能,所以我们有sendx和recvx分别指向下一次写 和 下一次读的位置,buf、sendx、recvx就构成了一个环形数组。
因为channel是用在多个goroutine之间的通信,所以需要一把Mutex来保护读写关闭操作的并发安全,又因为channel提供一个读和写等待队列来帮助goroutine在未完成读写操作后,可以被阻塞挂起(将goroutine放入队列),然后等待channel通信来临再次被唤醒。
type sudog struct {g *g // 锁定的goroutinenext *sudogprev *sudogelem unsafe.Pointer // data element (may point to stack)acquiretime int64releasetime int64ticket uint32isSelect boolsuccess boolwaiters uint16parent *sudog // semaRoot binary treewaitlink *sudog // g.waiting list or semaRootwaittail *sudog // semaRootc *hchan // channel
}
sudg是对阻塞挂起goroutine的一个封装,用多个sudg来构成等待队列。
2.channel的操作
1.channel写入
- channel中有读等待goroutine
- 从recvq取出队列头部的sudg,将要写入的数据拷贝到sudg对应的elem容器上,唤醒sudg绑定的g
- channel中没有读等待goroutine,并且环形缓冲区数组里面有剩余空间
- 将数据写到sendx的位置
- channel中没有读等待goroutine,并且无剩余空间存放数据
- 获取一个sudg结构,绑定对应的goroutine,channel,ep指针
- 将sudg放入channel的写等待队列
- 使用runtime.gopark()挂起当前goroutine
- 写入的channel为nil
- 对nil的channel进行写操作,会导致当前goroutine永久性挂起
- 若在main 函数则直接fatal error
- channel已经关闭,还想进行写操作
- 直接panic
2.channel读取
- channel中有写等待goroutine
- 从写等待队列里面拿到头部sudog,进入recv流程
- 如果channel无缓冲区,直接取出sudog里面的数据,并唤醒sudog里的g
- 如果channel有缓冲区,先尝试读取环形数组recvx对应的元素,并将sudog中的元素写入到缓冲区,唤醒sudog对应的g
- channel中没有写等待goroutine,并且环形缓冲数组中有剩余元素
- 读取recvx的数据,recvx++,qcount–
- channel中没有写等待goroutine,并且环形缓冲数组中无剩余元素
- 获取一个sudog结构,绑定channel,goroutine,ep指针,将sudog放入channel的读等待队列,挂起当前goroutine
- 读取的channel为nil
- main函数中直接fatal error;goroutine中永久挂起
- channel已经关闭,并且buf里面没有元素
- 读取对应类型的零值
selelect
核心原理:按照随机顺序执行case,直到某个case操作完成,如果所有case的都没有完成,则看有没有default分支,如果有,就直接走default,防止阻塞。如果没有的话,就将当前goroutine加入到所有case对应channel的等待队列中,等待唤醒。如果当前goroutine被某一个case上的channel操作唤醒后,还需要将当前goroutine从所有case对应channel的等待独队列中剔除。
面试题
1.channel是线程安全的吗?为什么?
是线程安全的。
因为channel的底层数据结构都是用同一把runtime.Mutex来进行保护的,对channel的操作只有读、写、关闭三种操作。在Go语言中,channel主要是让goroutine之间传递数据和进行同步操作,通过channel,可以保证数据一致性和并发安全性。hchan的底层实现中,hchan结构体采用Mutex锁来保护数据读写安全。在对循环数组buf中的数据进行入队和出队操作时,必须先获取互斥锁,才能操作channel数据。
2.channel的底层实现原理
channel的底层数据结构叫做runtime.hchan,在hchan拥有一把runtime.Mutex来保证读写关闭操作逻辑的并发安全,通过读写指针和一个buf数组实现了一个环形缓冲队列,让channel拥有存储数据的能力;还拥有读写等待队列,当一个goroutine对channel进行读或写操作,操作无法及时完成的时候,可以进入队列等待,当前goroutine也被runtime.gopark挂起;读写操作也能取出等待队列里面的goroutine,通过runtime.goready将等待中的goroutine唤醒,等待GMP的调度。
3.对channel进行读写关闭操作?
1.对nil的channel进行读和写都会造成永久性阻塞,如果是在main函数 直接fatal error,关闭发生panic。
2.对不为nil,且未关闭的channel操作,读和写都有两种情况:
- 读操作
- 成功读取:如果channel中有数据,直接读取channel,如果此时等待队列recvxq里面有goroutine,那么需要将队列头部goroutine写入channel,唤醒这个goroutine;如果channel没有数据,就尝试从写等待队列中读取数据,并做对应的唤醒操作。
- 阻塞挂起(读操作无法及时完成):channel里面没有数据并且写等待队列为空,则当前goroutine加入等待队列中,并挂起,等待唤醒。
- 写操作
- 成功写入:如果channel读等待队列不为空,则取头部goroutine,将数据直接复制给这个头部的goroutine,并将其唤醒,流程结束;否则就尝试将数据写入到channel环形缓冲中。
- 阻塞挂起(写操作无法及时完成):通道里面buf满了并且等待队列为空,则当前goroutine加入写等待队列中,并挂起,等待唤醒。
3.对已经关闭的channel进行写和关闭操作都会panic,而读取是直到读完channel中剩余的数据,还想读的话,就会获得零值。
算法题
1.select中case的使用
func case1() {c1 := make(chan int)c2 := make(chan int)close(c1)close(c2)select {case <-c1:fmt.Println("c1")case c2 <- 1:fmt.Println("c2")default:fmt.Println("default")}
}
请问以上程序的输出结果?
select中的case调用是随机的,不确定到底先调用哪一个case使用。
func case2() {c := make(chan int, 1)done := falsefor !done {select {case <-c:print(1)c = nilcase c <- 1:print(2)default:print(3)done = true}}
}
有缓冲的channel,在写操作的时候,如果缓冲区未满那么直接写入数据,否则阻塞;在读操作的时候,如果有数据那么直接读取,否则阻塞。
2.有4个goroutine,按编号1、2、3、4循环打印
func main() {ch := make([]chan int, 4)for i, _ := range ch {ch[i] = make(chan int)go func(i int) {for {v := <-ch[i]fmt.Println(v + 1)time.Sleep(time.Second)ch[(i+1)%4] <- (v + 1) % 4}}(i)}ch[0] <- 0select {}
}