面试时、我把Go 的 chan 源码分析给面试官看

在 Go 语言并发编程中,“不要通过共享内存来通信,而应该通过通信来共享内存”是其核心理念之一。这句话体现了 Go 语言并发模型的设计哲学,即通过通信机制(如 channel)来实现数据共享,而不是直接操作共享内存。

一、chan的优势

  • 避免竞态条件:直接操作共享内存容易引发竞态条件,即多个线程同时访问和修改同一内存区域,导致数据不一致。而通过 channel 通信,数据的传递是原子性的,避免了这种问题。
  • 简化并发逻辑:使用通信机制可以将程序解耦为多个独立的模块,每个模块负责特定任务,通过 channel 传递消息。这种方式使得并发逻辑更加清晰,易于理解和维护。
  • 提高代码安全性:Go 语言的 channel 设计确保了数据在并发环境下的安全访问,减少了因共享内存导致的错误。

二、基础概念

  • Goroutine:Goroutine 是 Go 语言中的轻量级线程,由 Go 运行时管理,创建和销毁成本低。多个 Goroutine 可以并发运行。
  • Channel:Channel 是 Go 语言实现通信的核心机制,用于在 Goroutine 之间传递数据。它支持同步和缓冲两种模式。通过 channel,Goroutine 可以安全地共享数据,而无需直接操作共享内存。

以下是一个简单的示例,展示如何通过 channel 在 Goroutine 之间共享数据:

package main

import (
	"fmt"
	"sync"
)

func main() {
	ch := make(chan int) // 创建一个无缓冲区的 channel
	var wg sync.WaitGroup

	wg.Add(1)
	go func() {
		defer wg.Done()
		ch <- 10 // 发送数据
	}()

	wg.Add(1)
	go func() {
		defer wg.Done()
		data := <-ch // 接收数据
		fmt.Println("Received:", data)
	}()

	wg.Wait()
}

在这个例子中,两个 Goroutine 通过 channel 安全地交换数据。特殊场景下的共享内存, 尽管 Go 语言倡导通过通信共享内存,但在某些特殊场景下,仍然可以通过 sync 包或 unsafe 包直接操作共享内存,但需要谨慎使用。

三、源码分析

首先 chan 的底层数据结构定义在 runtime/chan.go 中:

type hchan struct {
	qcount   uint           // 表示chan 里面的元素数量
	dataqsiz uint           // chan 底层循环数组的长度
	buf      unsafe.Pointer // 指向底层循环数组的指针, 对于有缓冲的通道
	elemsize uint16 		// 表示chan 里面的元素大小
	closed   uint32			// 判断 chan 是否被关闭
	timer    *timer 		// timer feeding this chan
	elemtype *_type 		// chan 中的元素种类
	sendx    uint   		// 已经发送元素在循环数组中的索引
	recvx    uint   		// 已经接受元素在循环数组中的索引
	recvq    waitq  		// 等待接收的goroutine 队列
	sendq    waitq  		// 等待发送的goroutine 队列

	// 因为 chan 被多个go 程共享, 所以需要加锁保护
	lock mutex
}

我画了一张图片表示这个chan 结构
在这里插入图片描述

首先我们先看一下 waitq 的基本结构, 这个结构是一个双向链表, 其中sudog 这个类型其实是对goroutine 这个数据结构的封装。

type waitq struct { 
	first *sudog
	last *sudog 
}

3.1 创建 chan

创建chan 的函数是 makechan, 因为chan 有两种类型, 第一种是缓冲类型, 第二种是非缓冲类型, 所以它的创建条件相对来说也是存在两种的。

var c *hchan
switch {
case mem == 0:
	c = (*hchan)(mallocgc(hchanSize, nil, true))
	c.buf = c.raceaddr()
case !elem.Pointers():
	c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
	c.buf = add(unsafe.Pointer(c), hchanSize)
default:
	c = new(hchan)
	c.buf = mallocgc(mem, elem, true)
}

case1: 表示 mem==0, 也就创建一个非缓冲区通道, 或者是元素大小为0,这种情况通常指的是 struct{}{} 这种数据类型,

  1. 直接分配大小为hchanSize的内存仅hchan结构体。
  2. c.buf指向c.raceaddr()(通常是unsafe.Pointer(&c.buf)), 用于竞态检测工具的同步点,无实际数据存储。

case2: 表示 elem.ptrdata == 0 表示当前元素不包含对应的指针

  1. 一次性分配hchanSize + mem的内存(结构体+缓冲区连续空间)。
  2. c.buf指向结构体末尾 add(unsafe.Pointer( c ), hchanSize)

case3: 元素包含指针, 同时分为两次进行分配, 第一次分配对应的hchan, 第二次分配对应的缓冲区 mallocgc(mem, elem, true)

首先我们看一下接收的过程, 因为接收其实是带ok 和 不带 ok 的形式, ok 表示当前的 chan 时候被关闭

func chanrecv1(c *hchan, elem unsafe.Pointer) {
	chanrecv(c, elem, true)
}

func chanrecv2(c *hchan, elem unsafe.Pointer) (received bool) {
	_, received = chanrecv(c, elem, true)
	return
}

3.2 外界接收数据

然后我们分析一下对应的 chanrecv 这个函数, 这个接收操作其实也是比较复杂的, 首先它有三个参数:

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) 
  • c: 要接收数据的 channel。
  • ep: 指向接收值存放地址的指针。若为 nil,表示调用者忽略接收值。
  • block: 布尔值,表示是否为阻塞模式。

当通道接收操作是 select 的多个 case 之一时,selected 表示该 case 是否被选中执行:

  • selected == true:该通道的接收操作是 select 最终选择的就绪操作。
  • selected == false:该通道的接收操作未被选中(例如其他 case 已就绪,或 default 分支存在且无就绪操作)
if !block && empty(c) { //如果是非阻塞而且当前的通道通道为空
	if atomic.Load(&c.closed) == 0 { //首先判断一下当前的通道时候存在数据
		// 如果没有返回对应的false
		return false, false
	}
}

如果当前的chan 已经被关闭了, 我们这个时候还需要判断一下当前的 chan 是否为空, 因为在检查的时候可能有数据被发送进来

if empty(c) {
	if raceenabled {
		raceacquire(c.raceaddr())
	}
	// 因为通道为null,所以需要清除对应的ep
	if ep != nil {
		typedmemclr(c.elemtype, ep)
	}
	// 因为会返回对应的0 值
	return true, false
}

如果使用代码表示上面的过程就是下面这段代码:

func chanTest1() {
	ch := make(chan int, 2)
	
	close(ch)

	select {
	case value := <-ch:
		fmt.Printf("chan has a value %d\n", value)
	default:
		fmt.Println("chan is null")
	}
}

如果被block了, 当 chan 已经被关闭, 并且循环数组buf 里面没有元素:

if c.closed == 1 {
	if c.qcount == 0 {
		if raceenabled {
			raceacquire(c.raceaddr())
		}
		unlock(&c.lock)
		// 返回对应类型的 零 值
		if ep != nil {
			typedmemclr(c.elemtype, ep)
		}
		return true, false
	}
}

下面一步是判断等待发送队列中存在对应的goroutine, 说明对应的 buf 是满的, 在这种情况下我们需要对chan 进行判断, 首先判断当前的chan 是缓冲通道还是非缓冲通道:

  • 非缓冲通道: 直接进行内存复制, 从sender->groutine 复制到 recver->groutine
  • 缓冲通道: 接收操作会从缓冲区头部取出一个值,并将发送者的值放入缓冲区尾部,然后返回 true, true。
if sg := c.sendq.dequeue(); sg != nil {
	recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
	return true, true
}

说完上面的内容, 就是通道双方互相接收的过程:

if c.qcount > 0 {
	qp := chanbuf(c, c.recvx)
	if raceenabled {
		racenotify(c, c.recvx, nil)
	}
	// 如果是 val := <- chan
	if ep != nil {
		typedmemmove(c.elemtype, ep, qp)
	}
	c.recvx++
	c.qcount--
}

接下来就是对应的处理被阻塞的情况

//1. 获取当前 goroutine
gp := getg()

//2. acquireSudog() 从池中获取一个 sudog 结构体
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
	mysg.releasetime = -1
}

//3.设置相关的字段

//4. 将 sudog 加入接收队列
c.recvq.enqueue(mysg)
if c.timer != nil {
	blockTimerChan(c)
}

//5. gopark 是 Go 运行时用于阻塞当前 goroutine 的函数。
gp.parkingOnChan.Store(true)
gopark(
	chanparkcommit, 
	unsafe.Pointer(&c.lock), 
	waitReasonChanReceive, 
	traceBlockChanRecv, 2)

之后就是等待对应的goroutine 被唤醒了:

// 检查当前 sudog(mysg)是否与 goroutine 的 waiting 字段一致。
// 如果不一致,说明等待队列被破坏,抛出异常(throw)。
if mysg != gp.waiting {
	throw("G waiting list is corrupted")
}

// 如果通道 c 有计时器(timer),调用 unblockTimerChan 解除计时器的阻塞状态。
// 这是为了确保计时器在 goroutine 被唤醒后能够正常工作。
if c.timer != nil {
	unblockTimerChan(c)
}

// 将 goroutine 的 waiting 字段设置为 nil,表示当前 goroutine 不再等待。
// 将 gp.activeStackChans 设置为 false,表示当前 goroutine 不再阻塞在通道上
gp.waiting = nil
gp.activeStackChans = false
if mysg.releasetime > 0 {
	blockevent(mysg.releasetime-t0, 2)
}
success := mysg.success

// 将 goroutine 的 param 字段设置为 nil,清除与 sudog 的关联。
// 将 mysg.c 设置为 nil,解除 sudog 与通道的关联。
gp.param = nil
mysg.c = nil
// 释放对应的block
releaseSudog(mysg)
return true, success

在这里插入图片描述

3.3 关闭chan

关于chansend 函数我们在这里就不进行分析了, 因为分析了chanrecv 函数之后, 再去看chansend函数之后就变得没有任何的压力了。

下面分析的函数是 closechan 这个函数, close chan 的接口其实非常简单只需要传递一个chan 参数:

close chan 参数首先会去检查对应的chan 是否被关闭, 如果被关闭会直接爆出panic 的错误

lock(&c.lock)
if c.closed != 0 {
	unlock(&c.lock)
	panic(plainError("close of closed channel"))
}

第二步是释放掉所有在等待队列中的发送者和接受者

// 将chan 中所有等待接收队列里 sudog 释放
for {
	// 从接收队列中出队一个sudog
	sg := c.recvq.dequeue()
	if sg == nil {
		break
	}
	// 相当于ep 指针, 直接赋值成为零值
	if sg.elem != nil {
		typedmemclr(c.elemtype, sg.elem)
		sg.elem = nil
	}
	if sg.releasetime != 0 {
		sg.releasetime = cputicks()
	}
	// 取出对应的goroutine
	gp := sg.g
	gp.param = unsafe.Pointer(sg)
	sg.success = false
	if raceenabled {
		raceacquireg(gp, c.raceaddr())
	}
	//形成一个链表
	glist.push(gp)
}

最终会遍历整个glist, 然后释放

	for !glist.empty() {
		gp := glist.pop() //取出最后一个
		gp.schedlink = 0
		goready(gp, 3) //唤醒
	}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值