什么是channel
简单来说就是一个阻塞队列,类似于java的ArrayBlockQueue。在协程之间提供通信。
生产者消费者示例
想体现channel的作用,使用简单易懂的生产者消费者示例最好不过了
func main(){
var waitgroup sync.WaitGroup//类似于java的countdownlatch
waitgroup.Add(2)
ch := make(chan int)
go producter(ch,&waitgroup)
go consumer(ch,&waitgroup)
waitgroup.Wait()//在两个协程运行结束之前,主线程阻塞
}
func consumer(ch chan int,waitgroup *sync.WaitGroup){
defer waitgroup.Done()
for{
a := <- ch
fmt.Println("消费者消费了:",a)
time.Sleep(time.Second)
}
}
func producter(ch chan int,waitgroup *sync.WaitGroup){
defer waitgroup.Done()
for i:=1;i<100;i++{
ch <- i
time.Sleep(time.Second*2)
}
Go相对于Java,使用多线程十分简洁方便。
channel数据结构
直接贴源码
type hchan struct {
qcount uint
dataqsiz uint
buf unsafe.Pointer
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 mutex
}
其中比较重要的变量有
- buf 循环链表,用来存放数据
- sendx,recvx 记录循环链表中发送接收的index
- sendq,recvq 存放Goroutine的抽象结构体的队列
- lock 锁
send和recv操作
每个操作,都首先需要加锁,然后把数据复制到循环链表里。
但是会有一些特殊情况,当channel满了后,生产者会阻塞;或者channel为空,消费者也会阻塞,那他们的调度流程是怎么样的。
channel满了
这时候作为生产者的Goroutine会抽象会一个结构体放入channel的sendq队列里,等待被唤醒,同时让出M,让M去被其他Goroutine使用
channel为空
作为消费者的Goroutine会抽象会一个结构体放入channel的recvq队列里,等待被唤醒,同时让出M,让M去被其他Goroutine使用
与上面不同的是,在有生产者生产消息时,不会调用channel的锁,然后插入数据。而是把数据直接复制给消费者。节省了消费者唤醒,拿锁再获取数据的时间。