Go-channel学习笔记

56 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第17天,点击查看活动详情

前言

Go中channel是一个核心类型,用于协程直接的通信以实现并发通信。

Go中对于并发编程有个核心思想:不要通过共享内存来通信,而是通过通信来共享内存,channel即是这一思想的实现。

简单使用

创建channel时需要指定一个传输类型,同时channel可以是单向或者双向。

  • chan T表示一个数据类型为T的双向通道类型,它可以接受和发送数据。
  • chan <- T表示一个数据类型为T的单向发送通道类型,它只可以发送数据。
  • <- chan T表示一个数据类型为T的单向接受通道类型,它只可以接受数据。
// 创建无缓冲通道
ch1 := make(chan int)
​
// 创建缓存区大小为10的通道
ch2 := make(chan int, 10)
​
// 向通道ch1发送数据
ch1 <- 5// 接受通道ch1的数据并赋值给i变量
i:= <-ch
​
// 查询通道缓冲区大小
cap(ch1)
​
// 查询该通道缓冲区中还有多少未接受的元素数量
len(ch1)
​
// 关闭通道,如果关闭一个nil通道或者已经关闭的通道会导致panic
close(ch1)
​
// 使用for range 读取chan
for i := range ch1{
    fmt.Println(i)
}
​
// 使用select监控多个chan,只会处理非堵塞case
// 1. 当chan为nil则对应case永久堵塞
// 2. 当chan已被close则对应case不会堵塞
// 3. 当多个chan都为非堵塞,则随机选择一个
for{
  select {
  case <-ch1:
    Todo1()
  case <-ch2:
    Todo2()
    }
}

底层数据结构

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 // 互斥锁
}

例如下面的例子:

func testChan() {
    ch := make(chan int, 10)
    waitGroup := sync.WaitGroup{}
    waitGroup.Add(2)
    go func() {
        defer waitGroup.Done()
        for i := 0; i < 10; i++ {
            ch <- i
        }
        close(ch)
    }()
​
    go func() {
        defer waitGroup.Done()
        for i := range ch {
            fmt.Println(i)
        }
    }()
    waitGroup.Wait()
}

当我们往ch通道写入数据时,可能会有以下几种情况

  1. buf循环数组还有空闲空间并且recvq接受队列为空,那么便使用lock互斥锁进行加锁,将数据复制到buf中,将sendx++,随后释放互斥锁。
  2. buf循环数组为空同时recvq接受队列有消费协程堵塞住,那么并不会加锁和复制到buf中,而直接将数据复制到消费协程待接受的变量地址
  3. buf循环数组已满同时recvq接受队列为空,那么将当前协程放入sendq发送队列中,并堵塞当前协程。