#Golang# Golang与并发编程(5) channel底层

#Golang# Golang与并发编程(5) channel底层

本文主要讨论 channel 底层的相关内容。


目录 Table of Contents


前情提要

为了实现 goroutine 通信,有两种常见并发模型:

  • 共享内存:使用共享内存方式,Go 中 sync 库包提供了多种同步的机制。
  • 消息队列:使用类似管道和消息队列的方式,各个并发单元数据相互独立,通过消息进行数据交换,Go 中 channel 类型模拟了这种同步的模式。

让我们再一次来复读 Go 社区的并发口号——“不要通过共享内存来通信,而应该通过通信来共享内存”。

本文将讨论 Go 并发编程中的通信桥梁 channel的底层,如有错漏,欢迎指出 ;P

数据结构

channel data structure

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
type hchan struct {
buf unsafe.Pointer // 信道数据缓冲队列(循环链表)
qcount uint // 信道数据元素个数
dataqsiz uint // 信道数据总计大小
elemsize uint16 // 信道数据元素大小
elemtype *_type // 信道数据元素类型
sendx uint // 信道数据待发送的索引,即循环链表的队头指针 front
recvx uint // 信道数据待接收的索引,即循环队列的队尾指针 rear

sendq waitq // 协程发送等待队列(双向链表)
recvq waitq // 协程接收等待队列(双向链表)

closed uint32 // 通道关闭标志
lock mutex // 互斥锁
}

type waitq struct {
first *sudog // 指向协程等待队列的头部
last *sudog // 指向协程等待队列的尾部
}

type sudog struct {
...
g *g // 阻塞协程的指针(包含状态)
elem unsafe.Pointer // 阻塞协程的数据(包含数据)
...
prev *sudog // 指向协程等待队列的前驱节点
next *sudog // 指向协程等待队列的后驱节点
...
}

Why Lock is Mutex?

  • channel 的应用虽然体现了“通过通信来共享内存”的思想,但是 channel 的实现则遵循“通过锁作悲观并发控制”的做法。
  • 由于 buf 和 waitq 均先进先出 (First In First Out),当 goroutine 并发地读写 channel 时,需要保证并发安全,使用互斥锁是很好理解的。

Why buf is Circylar Linked List?

  • 任何节点可以作头节点开始遍历,只需要维护一个尾指针。
  • 简化了 sendx 和 recvx 的变化。
  • 使用 qcount 而非 空余单元 消除循环链表的二义性,减少额外数据结构开销,可以直接返回 buf 链表长度。

Why waitq is Double Linked List?

  • 既可以访问前驱节点,也可以访问后驱节点。

算法流程

总览

channel communication flow

  • “不要通过共享内存来通信”:不要把数据放在共享区,通过限制同一时间的来访者,达到数据通信和并发安全的目的。

  • “应该通过通信来共享内存”:应该用信道作为中间者,通过读写该并发安全的第三方数据结构,实现数据通信和并发安全。

发送

channel send flow

channel 未满

send data to channel

send data to channel

  1. 加锁
  2. 拷贝 goroutine 数据到 channel 中
  3. 解锁

channel 已满

schedule goroutine to wait

enqueue channel

  1. goroutine 执行语句 ch <- data,向 channel 的信道数据缓冲队列 buf 中写入拷贝数据(此时 buf 已满),触发 goroutine scheduler 执行操作 gopark
  2. G 让出所占用的 M,进入 waiting 状态,协程阻塞
  3. 已阻塞的 goroutine 被抽象为 sudog,入队 channel 的协程发送等待队列 sendq

dequeue channel and schedule goroutine to ready

  1. goroutine 执行语句 <- ch,从 channel 的信道数据缓冲队列 buf 中读取拷贝数据(此时 buf 非满),触发 goroutine scheduler 执行操作 goready

  2. sudog 出队 channel 的协程发送等待队列 sendq,elem 进入 buf,g 更改 status

  3. G 放入 P 队列中,进入 ready 状态,协程可运行

接收

channel receive flow

channel 未空

receive data from channel

receive data from channel

  1. 加锁
  2. 拷贝 channel 数据到 goroutine 中
  3. 解锁

channel 已空

schedule goroutine to wait

receive data from channel

  1. goroutine 执行语句 <- ch,向 channel 的信道数据缓冲队列 buf 中读取拷贝数据(此时 buf 已空),触发 goroutine scheduler 执行操作 gopark

  2. G 让出所占用的 M,进入 waiting 状态,协程阻塞

  3. 已阻塞的 goroutine 被抽象为 sudog,入队 channel 的协程发送等待队列 recvq

dequeue channel and schedule goroutine to ready

  1. goroutine 执行语句 ch <- data,直接由 g1 拷贝数据到 g2(不经过 buf,不使用 lock),触发 goroutine scheduler 执行操作 goready
  2. sudog 出队 channel 的协程发送等待队列 recvq,elem 进入 g2,g1 更改 status
  3. G 放入 P 队列中,进入 ready 状态,协程可运行

关闭

channel close flow

  1. 排除 panic 情况,亦即关闭空的信道和重复关闭信道
  2. 遍历 sendq 和 recvq 并出队,sudog 中的 g 加入 gList 等待唤醒,sudog 中的 elem 置为 nil 将被清除
  3. 遍历 gList 并出栈,触发 goready 调度

参考链接


Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×