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

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

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


目录 Table of Contents


前情提要

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

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

让我们再一次来复读 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 的实现则遵循“通过锁作悲观并发控制”的做法。
  • 由于 bufwaitq 均先进先出 (First In First Out),当 goroutine 并发地读写 channel 时,需要保证并发安全,使用互斥锁是很好理解的。

Why buf is Circylar Linked List

  • 任何节点可以作头节点开始遍历,只需要维护一个尾指针。
  • 简化了 sendxrecvx 的变化。
  • 使用 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 的协程发送等待队列 sendqelem 进入 bufg 更改 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 的协程发送等待队列 recvqelem 进入 g2g1 更改 status
  3. G 放入 P 队列中进入 ready 状态,协程可运行

关闭

channel close flow

  1. 排除 panic 情况,亦即关闭空的信道和重复关闭信道
  2. 遍历 sendqrecvq 并出队,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

×