go 源码分析 channel 实现原理

chan

以后都在 github 更新,请戳 go channel 实现原理

目录

相关位置文件

内存构造

创建

send

recv

select

示例

相关位置文件

  • src/runtime/chan.go

内存构造

hchan.png

qcount 表示当前队列中存储了多少个元素(当前数量)

dataqsiz 表示环形队列的的大小(最大数量)

buf 指向环形队列所在内存的起始位置

elemsize 是每个元素占用的大小(单位为字节)

closed 表示当前的通道是否已关闭

稍后会解释 sendxrecvx

recvq 是一个 goroutine 接收者队列, 链表存储

sendq 是一个 goroutine 发送者队列, 链表存储

lock 是用作保护 hchan 数据结构的锁

创建

管道的创建最终都会调用到 src/runtime/chan.go 中的 makechan 函数

对于不同的元素大小, 会使用不同的内存分配策略

switch {
    case mem == 0:
        // Queue or element size is zero.
        c = (*hchan)(mallocgc(hchanSize, nil, true))
        // Race detector uses this location for synchronization.
        c.buf = c.raceaddr()
    case elem.ptrdata == 0:
        // Elements do not contain pointers.
        // Allocate hchan and buf in one call.
        c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
        c.buf = add(unsafe.Pointer(c), hchanSize)
    default:
        // Elements contain pointers.
        c = new(hchan)
        c.buf = mallocgc(mem, elem, true)
}

send

//  c <- x 编译后的代码执行入口
//go:nosplit
func chansend1(c *hchan, elem unsafe.Pointer) {
	chansend(c, elem, true, getcallerpc())
}

chansend 会锁住 lock 这把锁, 如果recvq 不为空的话, 则尝试从里面弹出一个 goroutine, 并把元素发送给这个弹出的 goroutine 处理. 如果 recvq 为空, 则尝试把这个元素存储到环形队列中, 如果环形队列满了的话, 则把当前协程加入 sendq 并阻塞当前的 goroutine(有一个 block 参数判断需不需要阻塞, 当前值为 true)

chansend

recv

// <- c 编译后的代码执行入口
//go:nosplit
func chanrecv1(c *hchan, elem unsafe.Pointer) {
	chanrecv(c, elem, true)
}

chanrecv 的流程和 chansend 的流程类似

首先, 锁住 lock 这把锁, 如果 sendq 不为空的话, 则尝试从里面弹出一个 goroutine 并获取该协程中存储的对应的元素, 如果 sendq 为空, 则尝试从环形队列中获取一个元素, 如果队列已经为空了, 则把当前协程加入recvq 并阻塞当前的 goroutine(有一个 block 参数判断需不需要阻塞, 当前值为 true)

chanrecv

select

select 被编译器处理后转换成以下两个函数

// 编译器实现
//
//	select {
//	case v = <-c:
//		... foo
//	default:
//		... bar
//	}
//
// 编译成
//
//	if selectnbrecv(&v, c) {
//		... foo
//	} else {
//		... bar
//	}
//
func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) {
	return chansend(c, elem, false, getcallerpc())
}

// 编译器实现
//
//	select {
//	case v, ok = <-c:
//		... foo
//	default:
//		... bar
//	}
//
// 编译成
//
//	if c != nil && selectnbrecv2(&v, &ok, c) {
//		... foo
//	} else {
//		... bar
//	}
//
func selectnbrecv(elem unsafe.Pointer, c *hchan) (selected bool) {
	selected, _ = chanrecv(c, elem, false)
	return
}

select 的发送和接收和 send 还有 recv 调用的是相同的函数, 唯一的区别是 block 参数由 true 变成了 false

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
	// ...
  	if !block {
			unlock(&c.lock)
			return false, false
	}
  // ...
}

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
  // ...
  	if !block {
			unlock(&c.lock)
			return false
	}
  // ...
}

chanrecvchansend 都会检查 block 参数来确认是否需要阻塞当前的 goroutine

示例

sendq 和 recvq

package main

var c chan int = make(chan int, 5)

func main() {
	for i := 0; i < 5; i++ {
		c <- i
	}
}

example

这是执行完 example 后的状态, dataqsiz 为 5 表示当前队列里有 5 个元素, buf 指向存储对象的内存块的起始位置, elemsize 为 8, 也是我的机器上 golang 的 int 的大小

closed 表示当前的通道是否已被关闭

如果我们再发送一次

c <- 6

example2

当前的 goroutine 会被添加到 sendq 的队列中, 并阻塞, 发送的元素的指针也会保存到当前的 goroutine 中, 存储在 sudog 结构中

如果我们再添加多一个发送方

package main

import "time"

var c chan int = make(chan int, 5)

func send() {
	time.Sleep(2 * time.Second)
	c <- 7
}

func main() {
	go send()
	for i := 0; i < 5; i++ {
		c <- i
	}
	c <- 6
}

example3

recvqsendq 的结构相同, 但是它存储的是所有阻塞在接收状态中的 goroutine 队列

sendx 和 recvx

如果我们运行如下的示例

package main

var c chan int = make(chan int, 5)

func main() {
	for i := 0; i < 5; i++ {
		c <- i
	}
	// break point 1
	<- c
	<- c
	// break point 2
	c <- 6
	c <- 7
	// break point 3
}

break point 1, 状态和 示例 开始时相似

breakpoint1

break point 2, recvx 往前移了 2 个位置, sendx 保持不变, qcount 变成了 3

breakpoint2

break point 3sendx 也往前移动了 2 个位置, qcount 变成了 5, 此时通道的元素存满了

breakpoint3

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值