前言
Go中channel是一个核心类型,用于协程直接的通信以实现并发通信。
Go中对于并发编程有个核心思想:不要通过共享内存来通信,而是通过通信来共享内存,channel即是这一思想的实现。
简单使用
创建channel时需要指定一个传输类型,同时channel可以是单向或者双向。
chan T
表示一个数据类型为T
的双向通道类型,它可以接受和发送数据。
chan <- T
表示一个数据类型为T的单向发送通道类型,它只可以发送数据。
<- chan T
表示一个数据类型为T的单向接受通道类型,它只可以接受数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| // 创建无缓冲通道 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() } }
|
底层数据结构
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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 // 互斥锁 }
|
例如下面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 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通道写入数据时,可能会有以下几种情况
buf
循环数组还有空闲空间并且recvq
接受队列为空,那么便使用lock
互斥锁进行加锁,将数据复制到buf
中,将sendx++,随后释放互斥锁。
buf
循环数组为空同时recvq
接受队列有消费协程堵塞住,那么并不会加锁和复制到buf
中,而直接将数据复制到消费协程待接受的变量地址
buf
循环数组已满同时recvq
接受队列为空,那么将当前协程放入sendq
发送队列中,并堵塞当前协程。