源码分析-Golang Chan

channel结构体

  • src/runtime/chan.go

  • 内部存储是以循环队列的形式存储额

  • type hchan struct {
    	qcount   uint           // 代表着这队列中总共有多少元素
    	dataqsiz uint           // 创建时的容量设置,既make时候的大小,
    	buf      unsafe.Pointer // 存储的数据
    	elemsize uint16 // chan数据对象的自身大小,既make的时候的参数类型的大小
    	closed   uint32  // 标识关闭状态
    	elemtype *_type // 代表chan中的元素类型
    	sendx    uint   // 待发送的索引,既循环队列的队尾指针tail
    	recvx    uint   // 待读取的索引,既循环队列的队头head
    	recvq    waitq  // 接收方 的等待队列
    	sendq    waitq  // 发送方 的等待队列
    	lock mutex
    }
    

channel的创建

  • 相关常量定义

    • const (
      	maxAlign  = 8
      	hchanSize = unsafe.Sizeof(hchan{}) + uintptr(-int(unsafe.Sizeof(hchan{}))&(maxAlign-1))
      	debugChan = false
      )
      
      • maxAlign: 这个与操作系统有关,cpu有三级缓存,L1,L2,L3,L3cpu之间共享,数据以cacheline的形式存储在寄存器中,读取的时候,会通过总线广播其他cpu,标识cache line 无效, 如果cache line 中存储的不是同一种数据,会使得触发伪共享问题,而maxAlign就是为了使得,该数据刚好占满整个cache line (空间换时间的做法)
      • hchansize: 与Java的hashMap类似,都是为了使得cap 为2的n次方
  • chan分为: 无缓冲chan和有缓存chan

    • 无缓冲既: make(chan int) : 这时候可以认为只是用于 通知
    • 有缓冲: make(chan int,10) : 可以用于数据传输
  • 源码分析

    • func makechan(t *chantype, size int) *hchan {
      	// 获取元素
      	elem := t.elem 
      
      	// 获取make的元素的所占内存大小,可以发现对象的大小是有限定
      	if elem.size >= 1<<16 {
      		throw("makechan: invalid channel element type")
      	}
      	// 不同的操作系统cacheline的大小是不同的,但是都是2的n次方
      	if hchanSize%maxAlign != 0 || elem.align > maxAlign {
      		throw("makechan: bad alignment")
      	}
      	// 当乘积过大,超出内存时候,panic
      	mem, overflow := math.MulUintptr(elem.size, uintptr(size))
      	if overflow || mem > maxAlloc-hchanSize || size < 0 {
      		panic(plainError("makechan: size out of range"))
      	}
      
      	// 注意: 当如果创建的对象不是指针的话,gc不会对该部分进行检测回收
      	var c *hchan
      	switch {
      	case mem == 0:
      		// 创建的是,无缓冲通道
      		// 申请内存空间,可以发现,当为无缓冲的通道的时候,创建的是空值
      		c = (*hchan)(mallocgc(hchanSize, nil, true))
      
      		c.buf = c.raceaddr()
      	case elem.ptrdata == 0:
      		// 如果 chan的数据非指针对象,则一次创建全部: 
      		c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
      		c.buf = add(unsafe.Pointer(c), hchanSize)
      	default:
      		// 当包含指针时,2次创建
      		c = new(hchan)
      		c.buf = mallocgc(mem, elem, true)
      	}
      
      	c.elemsize = uint16(elem.size)
      	c.elemtype = elem
      	c.dataqsiz = uint(size)
      	lockInit(&c.lock, lockRankHchan)
      
      	if debugChan {
      		print("makechan: chan=", c, "; elemsize=", elem.size, "; dataqsiz=", size, "\n")
      	}
      	return c
      }
      

chan的发送

  • func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
    // 如果为空,且阻塞,直接挂起该goroutine跑出异常
    	if c == nil {
    		if !block {
    			return false
    		}
    		gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
    		throw("unreachable")
    	}
    
    	.... 省略一些debug 
    
    	// 写数据的时候是必须要加锁的,但是加锁的性能损耗大,所以加锁前会先提前判断是否可以退出
    	// 当非阻塞写 + 未关闭+ 如果数据满了
    	if !block && c.closed == 0 && full(c) {
    		return false
    	}
    
    	var t0 int64
    	if blockprofilerate > 0 {
    		t0 = cputicks()
    	}
    	
    	// 开始加锁
    	lock(&c.lock)
    	
    	// 安全性校验
    	if c.closed != 0 {
    		unlock(&c.lock)
    		panic(plainError("send on closed channel"))
    	}
    	// 如果当前等待队列中有在等待的,则会直接将该数据dropoff给这个receiver,不将数据保存到buf中
    	if sg := c.recvq.dequeue(); sg != nil {
    		send(c, sg, ep, func() { unlock(&c.lock) }, 3)
    		return true
    	}
    	
    	// 如果还有空间剩余
    	if c.qcount < c.dataqsiz {
    		qp := chanbuf(c, c.sendx)
    		if raceenabled {
    			racenotify(c, c.sendx, nil)
    		}
    		// 将该实例数据拷贝buf中
    		typedmemmove(c.elemtype, qp, ep)
    		// 然后更新索引,环形数组的话,下次还会从0开始
    		c.sendx++
    		if c.sendx == c.dataqsiz {
    			c.sendx = 0
    		}
    		// 更新拥有的数据数量
    		c.qcount++
    		unlock(&c.lock)
    		return true
    	}
    	// 进入这,说明没有空间剩余了,如果非阻塞,则直接return(既select-default的做法)
    	if !block {
    		unlock(&c.lock)
    		return false
    	}
    
    	
    	// 开始挂起,获取当前的goroutine
    	gp := getg()
    	// 获取sudog ,打包当前对象,并且与当前的g所绑定
    	mysg := acquireSudog()
    	mysg.releasetime = 0
    	if t0 != 0 {
    		mysg.releasetime = -1
    	}
    	mysg.elem = ep
    	mysg.waitlink = nil
    	mysg.g = gp
    	mysg.isSelect = false
    	mysg.c = c
    	gp.waiting = mysg
    	gp.param = nil
    	// 更新sudog到发送的等待队列中
    	c.sendq.enqueue(mysg)
    	// 调用gopark 进行阻塞
    	atomic.Store8(&gp.parkingOnChan, 1)
    	gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
    
    	KeepAlive(ep)
    
     // 被唤醒之后,先进行安全性校验,如果当前的sudog与设置的不符合,则认为是数据被破坏了
    	if mysg != gp.waiting {
    		throw("G waiting list is corrupted")
    	}
    	gp.waiting = nil
    	gp.activeStackChans = false
    	closed := !mysg.success
    	gp.param = nil
    	if mysg.releasetime > 0 {
    		blockevent(mysg.releasetime-t0, 2)
    	}
    	mysg.c = nil
    	// 然后开始释放
    	releaseSudog(mysg)
    	if closed {
    		if c.closed == 0 {
    			throw("chansend: spurious wakeup")
    		}
    		panic(plainError("send on closed channel"))
    	}
    	return true
    }
    
  • func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
    	// 如果接收方是有实例数据等待接收
    	if sg.elem != nil {
    	// 则直接将数据拷贝到receiver的接收数据结构中
    		sendDirect(c.elemtype, sg, ep)
    		sg.elem = nil
    	}
    	gp := sg.g
    	unlockf()
    	gp.param = unsafe.Pointer(sg)
    	sg.success = true
    	if sg.releasetime != 0 {
    		sg.releasetime = cputicks()
    	}
    	// 最后唤醒接收方goroutine
    	goready(gp, skip+1)
    }
    

channel的接收

  • // 第一个参数是chan的指针, 第二个参数是
    func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
    	// 与send的时候同理,对于nil channel直接挂起
    
    	// 同样,与send 同理,会有fastFail 校验,来避免加锁
    	if !block && empty(c) {
    
    		if atomic.Load(&c.closed) == 0 {
    			return
    		}
    		if empty(c) {
    			if raceenabled {
    				raceacquire(c.raceaddr())
    			}
    			if ep != nil {
    				typedmemclr(c.elemtype, ep)
    			}
    			return true, false
    		}
    	}
    
    	var t0 int64
    	if blockprofilerate > 0 {
    		t0 = cputicks()
    	}
    	// 加锁
    	lock(&c.lock)
    	// 如果chan已经close了,并且没有任何数据
    	if c.closed != 0 && c.qcount == 0 {
    		if raceenabled {
    			raceacquire(c.raceaddr())
    		}
    		// 则直接解锁,然后
    		unlock(&c.lock)
    		if ep != nil {
    			// 将返回值置为0值
    			typedmemclr(c.elemtype, ep)
    		}
    		return true, false
    	}
    	// 如果有sender 阻塞,则直接pop出第一个,直接从发送方获取数据
    	if sg := c.sendq.dequeue(); sg != nil {
    		recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
    		return true, true
    	}
    	
    	// 没有sender在发送数据,同时缓冲中有数据,则直接从缓冲中读取数据
    	if c.qcount > 0 {
    
    		// 则通过recvindex 得到数据
    		qp := chanbuf(c, c.recvx)
    		if raceenabled {
    			racenotify(c, c.recvx, nil)
    		}
    		// 然后将数据内容拷贝到参数中
    		if ep != nil {
    			typedmemmove(c.elemtype, ep, qp)
    		}
    		typedmemclr(c.elemtype, qp)
    		c.recvx++
    		if c.recvx == c.dataqsiz {
    			c.recvx = 0
    		}
    		c.qcount--
    		unlock(&c.lock)
    		return true, true
    	}
    	// 没数据,且异步,则直接返回
    	if !block {
    		unlock(&c.lock)
    		return false, false
    	}
    	// 说明什么数据也没有,开始挂起该goroutine
    	gp := getg()
    	// 获取sudog,打包成sudog,将sudog 与当前goroutine绑定
    	mysg := acquireSudog()
    	mysg.releasetime = 0
    	if t0 != 0 {
    		mysg.releasetime = -1
    	}
    	
    	mysg.elem = ep
    	mysg.waitlink = nil
    	gp.waiting = mysg
    	mysg.g = gp
    	mysg.isSelect = false
    	mysg.c = c
    	gp.param = nil
    	// 将该recv的goroutine入队
    	c.recvq.enqueue(mysg)
    
    	atomic.Store8(&gp.parkingOnChan, 1)
    	gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)
    
    	// 安全性检测
    	if mysg != gp.waiting {
    		throw("G waiting list is corrupted")
    	}
    
    	gp.waiting = nil
    	gp.activeStackChans = false
    	if mysg.releasetime > 0 {
    		blockevent(mysg.releasetime-t0, 2)
    	}
    	success := mysg.success
    	gp.param = nil
    	mysg.c = nil
    	releaseSudog(mysg)
    	return true, success
    }
    
  • 如果有sender 在等待队列中等待,则直接从sender中拿去数据
    func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
    	// 如果是无缓冲的chan
    	if c.dataqsiz == 0 {
    		if raceenabled {
    			racesync(c, sg)
    		}
    		if ep != nil {
    			// 从直接从sender的sudog 中提取数据
    			recvDirect(c.elemtype, sg, ep)
    		}
    	} else {
    		进入到这里,说明缓冲是满的
    		1. 从缓冲中拿去数据
    		2. 更新recvIndex
    		3. 更新sendIndex
    		qp := chanbuf(c, c.recvx)
    		if raceenabled {
    			racenotify(c, c.recvx, nil)
    			racenotify(c, c.recvx, sg)
    		}
    		// copy data from queue to receiver
    		if ep != nil {
    			typedmemmove(c.elemtype, ep, qp)
    		}
    		// copy data from sender to queue
    		typedmemmove(c.elemtype, qp, sg.elem)
    		c.recvx++
    		if c.recvx == c.dataqsiz {
    			c.recvx = 0
    		}
    		c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz
    	}
    	sg.elem = nil  // 可以认为是快速清除引用 
    	gp := sg.g 
    	unlockf()
    	gp.param = unsafe.Pointer(sg)
    	// 然后跟新sender 中的sudog 为success
    	sg.success = true
    	if sg.releasetime != 0 {
    		sg.releasetime = cputicks()
    	}
    	// 通知sender ,唤醒sender
    	goready(gp, skip+1)
    }
    

总结

  • 数据通信不是通过共享内存的形式来通信,而是通过 copy 内存的形式,既发送和接收得到数据都会触发拷贝操作

  • golang的hchan,内部是回环数组实现数据的存储

  • 对于nilchannel 会触发挂起

  • 无论是send还是recv,当都会有一个快速失败的校验,因为数据写入和读取都是需要加锁的

  • 无论是send还是recv,都会先判断是否 recv队列和send队列是否为空,不为空的话

    • 对于recv而言,则会先直接从发送队列中取出sudog,然后从环形队列中取出值给recv之后,将发送队列的sudog的值追加到环形队列中
    • 对于send而言,则直接从recv队列中拿出sudog,然后把要发送的数据直接转交给这个receiver
    • 最后,都会唤醒从队列取出的sudog所绑定的goroutine

问题

  • chan无缓冲和有缓冲的区别

    • 无缓冲的话,初始化make的时候,申请内存的时候是一个空值
    • 无缓冲的chan,读写必须在不同的routine
  • 当chan为无缓冲的时候,send和recv会做什么

    • send和recv 必须是 不在同一个routine才行
  • recv的时候什么情况下,参数会为空

    • 当 <-c 的时候,参数是空参数
  • closed := !mysg.success 的触发情况有哪些

    • 当close的时候,close的时候会将等待队列中所有的succes都设置为false
  • send 什么时候被唤醒,是怎么被唤醒的

    • when: 如果buffer满的时候,send的goroutine会被打包成sudog,然后丢入到发送队列中,等待被唤醒
    • how: 通过goready 被唤醒,当recv的时候,如果有send在等待,则会将数据从缓冲数据取出来之后,唤醒等待队列中的头节点
  • 什么是sudog

    • 代表的是一个 g 的等待队列,因为g 会发生复用,一个g所关联的同步对象可能是n个,

    • 每个sudog代表的都是g所关联的一个对象

  • 怎样的才是 非阻塞的发送数据给chan

    • 配合select 的发送,既是异步发送
  • chan 非指针对象和指针对象的区别

    • 内存分配都是一次性分配的
    • 非指针对象,在申请内存的时候,只会申请一次(既chan的固定大小+计算得出的mem大小)
    • 指针对象: 会触发2次申请内存,第一次为先申请hchan的内存,然后申请指针的内存
  • recvq和sendq的作用

    • recvq用于存储 <-c 这个操作的routine成员,当sendQ和没没有数据的时候,会打包成sudog,然后挂起
    • sendq用于存储 c<- 这个操作的goroutine,当buffer满的时候,则会打包成sudog ,然后添加到sendq,然后挂起
  • 会发现在hchan中的有几个成员变量似乎是冲突的,如 qcount ,elemsize ,不都是标识长度的吗

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值