channel

Posted by Liao on 2022-04-20

一、定义

Channel 是 goroutine 之间的通信方式,Go语言的并发模型是CSP(Communicating Sequential Processes),提倡通信来共享内存,而不是共享内存来通信。

gorotine由 runtime来管理

二、创建

创建有缓冲的缓存

1
ch := make(chan int, 2)   //创建channel,类型为char, buffer大小是2

无缓冲的channel

1
ch := make(chan int)   //创建channel

ch是存在于函数栈帧上的指针,指向对上的hchan数据结构

底层数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type hchan struct {
qcount uint // total data in the queue
dataqsiz uint // size of the circular queue
buf unsafe.Pointer // points to an array of dataqsiz elements
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 protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
lock mutex
}
  • lock:是互斥锁。channel需要支持goroutine间的并发访问,所以需要锁来保护整个数据结构
  • buf:对于有缓冲区的channel特有的结构,用于缓存数据。是个环形链表
  • qcount:缓冲区最多存储元素的个数
  • elemtype:每个元素占多大空间
  • elemtype *_type:golang运行时中(runtime)内存复制、垃圾回收等机制,依赖数据的类型信息,因此需要有该指针,指向元素类型的类型元数据
  • sendx/recvx:接收和发送指针 channel支持交替地读和写,需要记录读(接收)、写(发送)的下标
  • recvq/sendq:接收和发送的等待队列,是双向队列。当读和写不能立即完成时,需要让当前协程在channel上等待。当条件满足时,要能够立即唤醒等待的协程,因此需要两个等待队列,分别针对读(接收)和写(发送)
  • closed:channel能够被关闭,要记录它的状态

三、发送&接收数据

3.1 发送&接收数据过程

1
2
3
4
5
6
7
8
9
10
11
ch := make(chan int, 5)
//g1发送6个数据到ch中,但buffer大小只有5
ch <- 1
ch <- 2
ch <- 3
ch <- 4
ch <- 5
ch <- 6

//g2接收数据
<-ch

1、由于没有协程在等待接收数据,故所有数据都写入缓冲区中。sendx从下标为0开始向后移动,移动到下标4时回重新返回下标0的位置,因此channel的缓冲区是环形缓冲区

2、此时缓冲区已经没有空闲位置,第6个数据无处可放。此时,g1会以sudog的形式,进入(存储)到发送等待队列(sendq)中。

sudog是一个链表,里面记载着g1、ch、元素等信息。

3、协程g2从ch中接收一个元素,recvx指向下一个位置。

4、此时有空位会唤醒sendq中的g1,将(sudog记录着的)数据6发送给ch,sendx向后移动,此时缓冲区再次满了,sendq队列为空;

3.2 发送数据

发送数据的写法

1
ch <- 10 //10发送到ch (协程向ch中发送数据)
非阻塞情况

1、缓冲区(buffer)还有位置

2、无缓冲区 && 但有协程在接收队列(recvq)等待接收数据

阻塞的情况

1、ch == nil

2、无缓冲区 && 没有协程等着接收数据

3、有缓冲区但已经满 && 没有协程等着接收数据

1
2
3
4
5
6
7
//避免阻塞的写法
select {
case ch <- 10:
...
default:
...
}

3.3接收数据

接收数据的写法:

1
2
3
<-ch //会将接收到的结果丢弃
v := <-ch //从ch接收,并且给v赋值
v,ok := <-ch //ok为false时表示channel已经关闭,此时v是channel元素类型的零值
非阻塞的情况

1、有缓冲区,且数据未满

2、满或无缓冲区 && 但sendq中有协程等着发送数据

阻塞的情况

1、ch == nil

2、无缓冲区 && 无协程在sendq等着发送

3、有缓冲区但没数据 && 无协程在sendq等着发送

避免发送阻塞的写法:

1
2
3
4
5
6
select {
case <-ch: //当不会阻塞时执行此处
...
default: //发生阻塞时执行此处
...
}

四、应用代码

一道笔试题:

通过channel和2个goroutine交替打印出12AB34CD56EF78GH910IJ1112KL1314MN1516OP1718QR1920ST2122UV2324WX2526YZ2728

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
39
package main

import (
"fmt"
)

var num = make(chan bool, 1) //数字channel
var char = make(chan bool) //字母channel
var done = make(chan bool, 1) //是否完成任务的标志

func main() {
num <- true
go solveNum()
go solveChar()
<-done
}

func solveNum() {
for i := 1; i <= 28; i += 2 {
<-num
fmt.Print(i)
fmt.Print(i + 1)
select {
case char <- true:
default:
}
}
done <- true //以数字结尾
}

func solveChar() {
for i := 'A'; i <= 'Z'; i += 2 {
<-char
fmt.Print(string(i))
fmt.Print(string(i + 1))
num <- true
}
}

五、参考

https://www.bilibili.com/video/BV1hv411x7we?p=29&vd_source=d85f289ebf4d31920c7178bbb563a0a4