Golang 通道channel源码走读

本文是对Golang channel的关键源码进行走读,并记录使用时容易犯的一些错误。示例可能来源于网络,对应的Golang版本是1.21.9,仅供个人学习使用。

使用语法和基本示例

1. 超时控制

func waitForStopOrTimeout(stopCh <-chan struct{
   
   }, timeout time.Duration) <- chan struct{
   
   } {
   
   
    stopChWithTimeout := make(chan struct{
   
   })
    go func() {
   
   
        select {
   
   
           case <-stopCh: // 收到停止信号
          case <-time.After(timeout): // 等待超时
        }
       close(stopChWithTimeout)
    }()
    return stopChWithTimeout
}

该函数返回一个管道,可用于在函数之间传递,但该管道会在指定时间后自动关闭。

2. 控制并发数

var token = make(chan int, 10) // 创建缓冲型channel, 容量为10

func main() {
   
   

    for _, w := range work {
   
   
        go func() {
   
   
          token <- 1
          w() // 执行任务,访问第三方动作在w()中
          <-token
        }()     
    }
}
  1. 如果把token <- 1放到func外层,就是控制系统goroutine的数量。
  2. channel控制子协程的方式,不如waitGroup、Context等更优雅。

数据结构

// channel 数据结构
type hchan struct {
   
   
    qcount   uint           // 当前 channel 中存在多少个元素;
    dataqsiz uint           // 当前 channel 能存放的元素容量;
    buf      unsafe.Pointer // channel 中用于存放元素的环形缓冲区;
    elemsize uint16         // channel 元素类型的大小;
    closed   uint32         // 标识 channel 是否关闭;
    elemtype *_type         // channel 元素类型;
    sendx    uint           // 发送元素进入环形缓冲区的 index;
    recvx    uint           // 接收元素所处的环形缓冲区的 index;
    recvq    waitq          // 因接收而陷入阻塞的协程队列;
    sendq    waitq          // 因发送而陷入阻塞的协程队列;
    
    lock mutex // 用来确保每个读/写操作对 channel 的修改是原子的;
}

// waitq 是 sudog 的双向链表,用于存放因接收而陷入阻塞的协程;
type waitq struct {
   
   
    first *sudog       // 队列头部
    last  *sudog       // 队列尾部
}

// sudog是对goroutine的封装
type sudog struct {
   
   

    g *g

    next *sudog     // 队列中的下一个节点;
    prev *sudog     // 队列中的上一个节点;
    elem unsafe.Pointer // 读取/写入 channel 的数据的容器;

    acquiretime int64
    releasetime int64
    ticket      uint32

    // 标识当前协程是否处在 select 多路复用的流程中
    // g.selectDone must be CAS'd to win the wake-up race.
    isSelect bool

    // success 表示跟 c通道通信的状态; 
    // true 代表 goroutine已激活,通道 c 中存在数据;
    // false 代表 goroutine 已被唤醒,但通道 c 中不存在数据。
    success bool

    parent   *sudog // semaRoot binary tree
    waitlink *sudog // g.waiting list or semaRoot
    waittail *sudog // semaRoot
    c        *hchan // 标识与当前 sudog 交互的 chan.
}

初始化

初始化的过程,主要是在堆上分配内存,并初始化channel的成员变量。


const (
    maxAlign  = 8
    hchanSize = unsafe.Sizeof(hchan{
   
   }) + uintptr(-int(unsafe.Sizeof(hchan{
   
   }))&(maxAlign-1))
    debugChan = false
)

func makechan(t *chantype, size int) *hchan {
   
   
    elem := t.Elem

    // 编译器代码越界检查
    if elem.Size_ >= 1<<16 {
   
   
       throw("makechan: invalid channel element type")
    }
    if hchanSize%maxAlign != 0 || elem.Align_ > maxAlign {
   
   
       throw("makechan: bad alignment")
    }
    // 计算 channel 需要的内存大小,以及是否超出最大值
    mem, overflow := math.MulUintptr(elem.Size_, uintptr(size))
    if overflow || mem > maxAlloc-hchanSize || size < 0 {
   
   
       panic(plainError("makechan: size out of range"))
    }

    // 根据类型,初始 channel,分为 无缓冲型、有缓冲元素为 struct 型、有缓冲元素为 pointer 型 channel;
    var c *hchan
    switch {
   
   
    case mem == 0:
       // Channel 容量或元素大小为0
       // 1. 非缓冲型: buf没用,直接指向chan起始地址
       c = (*hchan)(mallocgc(hchanSize, nil, true))
       // Race detector uses this location for synchronization.
       c.buf = c.raceaddr()
    case elem.PtrBytes == 0:
       // 缓冲的 struct 型: 一次性分配好 96 + mem 大小的空间,并且调整 chan 的 buf 指向 mem 的起始位置;
       // (只进行一次内存分配操作)
       c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
       c.buf = add(unsafe.Pointer(c), hchanSize)
    default:
       // 有缓冲的 pointer 型,则分别申请 chan 和 buf 的空间,两者无需连续(两次内存分配操作)
       c = new(hchan)
       c.buf = mallocgc(mem, elem, true)
    }

    c
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值