Go channel底层原理与使用实例

1,017 阅读2分钟

什么是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的锁,然后插入数据。而是把数据直接复制给消费者。节省了消费者唤醒,拿锁再获取数据的时间。