ch4-channel

本文详细介绍了Go语言中基于CSP(CommunicatingSequentialProcesses)的并发模型,包括goroutine和channel的使用。通过示例展示了如何利用channel进行同步、解耦生产者和消费者,以及控制并发数。还深入探讨了channel的内部数据结构和操作,如创建、发送、接收及关闭channel的机制,并提到了可能引发goroutine泄露的情况。
摘要由CSDN通过智能技术生成

什么是CSP?

描述并发系统交互的模型,在Go中基于goroutine和channel实现。

通道的作用?

停止信号

定时执行任务

select{
case<-time.Tick(time.Second);
case<-s.stopc:
return
}

解耦生产者和消费者

func main()  {
	taskCh:=make(chan int,100)
	go work(taskCh)
	for i:=0;i<10;i++{
		taskCh<-i
	}
	time.Sleep(time.Second*3)
}

func work(taskCh <-chan int)  {
	const N=5
	for i:=0;i<N;i++{
		go func (id int)  {
			for { 
				  task:=<-taskCh
			    fmt.Printf("task:%d,id:%d\n",task,id)
			    time.Sleep(time.Second)
			}
		}(i)
	}
}

控制并发数

token:=make(chan int,10)
for _,w:=range work{
		go func ()  {
			token<-1
			w()
			<-token
		}()
}

数据结构


type hchan struct {
    // 队列中目前的元素计数
    qcount uint  
    // 环形队列的总大小 
    dataqsiz uint  
    // 指向循环数组
    buf unsafe.Pointer   
    // 元素的大小 
    elemsize uint16
    // 是否已被关闭
    closed uint32
    // runtime._type,代表 channel 中的元素类型的 runtime 结构体
    elemtype *_type  
    // 发送索引
    sendx uint  
    // 接收索引
    recvx uint  
    // 接收 goroutine 对应的 sudog 队列
    recvq waitq  
    // 发送 goroutine 对应的 sudog 队列
    sendq waitq  
		//保证读写channel过程为原子操作
    lock mutex
}

创建(makechan)

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")
    }

    if size < 0 || uintptr(size) > maxSliceCap(elem.size) || uintptr(size)*elem.size > _MaxMem-hchanSize {
        panic(plainError("makechan: size out of range"))
    } 

    var c *hchan
 
    switch {
    case size == 0 || elem.size == 0:
	      //如果无缓冲区或者元素为空的结构体
        c = (*hchan)(mallocgc(hchanSize, nil, true))
        //buf指向c的地址  
        c.buf = unsafe.Pointer(c)
    case elem.kind&kindNoPointers != 0:
     
        // 通过位运算知道 channel 中的元素不包含指针
      
       
        // 这种情况下 gc 不会对 channel 中的元素进行 scan
        c = (*hchan)(mallocgc(hchanSize+uintptr(size)*elem.size, nil, true))
        c.buf = add(unsafe.Pointer(c), hchanSize)
    default:
        // 元素含有指针,两次分配空间的函数 new/mallocgc
        c = new(hchan)
        c.buf = mallocgc(uintptr(size)*elem.size, elem, true)
    }
		//其他字段赋值
    c.elemsize = uint16(elem.size)
    c.elemtype = elem
    c.dataqsiz = uint(size)

    return c
}

发送(chansend)


func chansend1(c *hchan, elem unsafe.Pointer) {
    chansend(c, elem, true, getcallerpc())
}

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
 
    if c == nil {
				//为nil并且不阻塞直接返回
				//向nil发送数据不panic,closed会
        if !block {
            return false
        }
   
        // 挂起当前 goroutine,会永远阻塞下去
        gopark(nil, nil, "chan send (nil chan)", traceEvGoStop, 2)
        throw("unreachable")
    }

	  //这个语句为什么不上锁?
		//因为是顺序执行,当判断c.dataqsiz == 0....时
		//c.close==0一定成立
		//即便在判断过程中c.close被其他goroutine修改
		//也不影响逻辑
    if !block && c.closed == 0 && ((c.dataqsiz == 0 && c.recvq.first == nil) ||
        (c.dataqsiz > 0 && c.qcount == c.dataqsiz)) {
        //非缓冲并且接收队列第一个sudog为nil
				//有缓冲但是缓冲满了
				return false
    }

    var t0 int64
    if blockprofilerate > 0 {
        t0 = cputicks()
    }
	
    lock(&c.lock)

    // channel 已被关闭,panic
    if c.closed != 0 {
        unlock(&c.lock)
        panic(plainError("send on closed channel"))
    }

    if sg := c.recvq.dequeue(); sg != nil {
       passing the channel buffer (if any).
        //如果接收队列有sudug
				//说明缓冲为空或者非缓冲
        //直接将元素拷贝给sudog
        send(c, sg, ep, func() { unlock(&c.lock) }, 3)
        return true
    }

    
    // 说明缓冲未满
    if c.qcount < c.dataqsiz {
        //指向发送下标
	      //发送下标位置接收发送过来的元素  
        qp := chanbuf(c, c.sendx)

        // 将 goroutine 的数据拷贝到 buffer 中
        typedmemmove(c.elemtype, qp, ep)

        //环形队列操作
        c.sendx++
        if c.sendx == c.dataqsiz {
            c.sendx = 0
        }        
        c.qcount++
        unlock(&c.lock)
        return true
    }

    if !block {
        unlock(&c.lock)
        return false
    }

     
    // 在 channel 上阻塞,receiver 会唤醒
		//获得当前的goroutine
    gp := getg()
    mysg := acquireSudog()
    mysg.releasetime = 0
    if t0 != 0 {
        mysg.releasetime = -1
    }
   
    // 打包 sudog
    mysg.elem = ep
    mysg.waitlink = nil
    mysg.g = gp
    mysg.isSelect = false
    mysg.c = c
    gp.waiting = mysg
    gp.param = nil

    // 将当 goroutine 打包后的 sudog 入队到 channel 的 sendq 队列中
    c.sendq.enqueue(mysg)

    // 将这个发送 g 从 Grunning -> Gwaiting
    // 进入休眠
    goparkunlock(&c.lock, "chan send", traceEvGoBlockSend, 3)

    // 唤醒后要执行的代码
    if mysg != gp.waiting {
        // 先判断当前是不是合法的休眠中
        throw("G waiting list is corrupted")
    }
    gp.waiting = nil
		//close时,也会将gp.param=nil
		//发送完成后,gp.param会指向包装自己的sudug
    if gp.param == nil {
				//向关闭的c中发送数据
        if c.closed == 0 {
            throw("chansend: spurious wakeup")
        }
        panic(plainError("send on closed channel"))
    }
    gp.param = nil
    if mysg.releasetime > 0 {
        blockevent(mysg.releasetime-t0, 2)
    }
    mysg.c = nil
    releaseSudog(mysg)
    return true
}
func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
    // receiver 的 sudog 已经在对应区域分配过空间
		// 直接拷贝数据
    if sg.elem != nil {
        sendDirect(c.elemtype, sg, ep)
        sg.elem = nil
    }
    gp := sg.g
    unlockf()
    gp.param = unsafe.Pointer(sg)
    if sg.releasetime != 0 {
        sg.releasetime = cputicks()
    }

    // Gwaiting -> Grunnable
    goready(gp, skip+1)
}

接收

//对应形式
func chanrecv1(c *hchan, elem unsafe.Pointer) {
    chanrecv(c, elem, true)
}

//对应形式:i;ok:<-ch
func chanrecv2(c *hchan, elem unsafe.Pointer) (received bool) {
    _, received = chanrecv(c, elem, true)
    return
}

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
  

    // 如果在 nil channel 上进行 recv 操作,那么会永远阻塞
    if c == nil {
        if !block {
            // 非阻塞的情况下
            // 要直接返回
      
            return
        }gopark(nil, nil, "chan receive (nil chan)", traceEvGoStop, 2)

      
        throw("unreachable")
    }

   
    if !block && (c.dataqsiz == 0 && c.sendq.first == nil ||
        c.dataqsiz > 0 && atomic.Loaduint(&c.qcount) == 0) &&
        atomic.Load(&c.closed) == 0 {
        // 非阻塞且没内容可收的情况下要直接返回
        // 两个 bool 的零值就是 false,false
        return
    }

    var t0 int64
    if blockprofilerate > 0 {
        t0 = cputicks()
    }

    lock(&c.lock)

    // 当前 channel 中没有数据可读
    // 直接返回 not selected
    if c.closed != 0 && c.qcount == 0 {
        unlock(&c.lock)
        if ep != nil {
						//清除接收变量的内存
            typedmemclr(c.elemtype, ep)
        }
        return true, false
    }

    // sender 队列中有 sudog 在等待
    // 直接从该 sudog 中获取数据拷贝到当前 g 即可
    if sg := c.sendq.dequeue(); sg != nil {
       
        recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
        return true, true
    }

    if c.qcount > 0 {
        // Receive directly from queue
        qp := chanbuf(c, c.recvx)

        // 直接从 buffer 里拷贝数据
        if ep != nil {
            typedmemmove(c.elemtype, ep, qp)
        }
        typedmemclr(c.elemtype, qp)
        // 接收索引 +1
        c.recvx++
        if c.recvx == c.dataqsiz {
            c.recvx = 0
        }
        // buffer 元素计数 -1
        c.qcount--
        unlock(&c.lock)
        return true, true
    }

    if !block {
        unlock(&c.lock)
        // 非阻塞时,且无数据可收
        // 始终不选中,这是在 buffer 中没内容的时候
        return false, false
    }

    
    gp := getg()
    mysg := acquireSudog()
    mysg.releasetime = 0
    if t0 != 0 {
        mysg.releasetime = -1
    }
   
    // 打包成 sudog
    mysg.elem = ep
    mysg.waitlink = nil
    gp.waiting = mysg
    mysg.g = gp
    mysg.isSelect = false
    mysg.c = c
    gp.param = nil
    // 进入 recvq 队列
    c.recvq.enqueue(mysg)

    // Grunning -> Gwaiting
    goparkunlock(&c.lock, "chan receive", traceEvGoBlockRecv, 3)

     
    // 被唤醒
    if mysg != gp.waiting {
        throw("G waiting list is corrupted")
    }
    gp.waiting = nil
    if mysg.releasetime > 0 {
        blockevent(mysg.releasetime-t0, 2)
    }
    closed := gp.param == nil
    gp.param = nil
    mysg.c = nil
    releaseSudog(mysg)
    // 如果 channel 未被关闭,那就是真的 recv 到数据了
    return true, !closed
}

func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
    if c.dataqsiz == 0 {
        if ep != nil {
           
            recvDirect(c.elemtype, sg, ep)
        }
    } else {
       
        qp := chanbuf(c, c.recvx)

       
        if ep != nil {
            typedmemmove(c.elemtype, ep, qp)
        }
       
        typedmemmove(c.elemtype, qp, sg.elem)
        c.recvx++
        if c.recvx == c.dataqsiz {
            c.recvx = 0
        }
        c.sendx = c.recvx // 考虑到缓冲满的情况
    }
    sg.elem = nil
    gp := sg.g
    unlockf()
    gp.param = unsafe.Pointer(sg)
    if sg.releasetime != 0 {
        sg.releasetime = cputicks()
    }

    // Gwaiting -> Grunnable
    goready(gp, skip+1)
}

关闭

func closechan(c *hchan) {
    // 关闭一个 nil channel 会直接 panic
    if c == nil {
        panic(plainError("close of nil channel"))
    }

    // 上锁,这个锁的粒度比较大,一直到释放完所有的 sudog 才解锁
    lock(&c.lock)

    // 在 close channel 时,如果 channel 已经关闭过了
    // 直接触发 panic
    if c.closed != 0 {
        unlock(&c.lock)
        panic(plainError("close of closed channel"))
    }

    c.closed = 1

    var glist *g

    // release all readers
    for {
        sg := c.recvq.dequeue()
        // 弹出的 sudog 是 nil
        // 说明读队列已经空了
        if sg == nil {
            break
        }

        // sg.elem unsafe.Pointer,指向 sudog 的数据元素
        // 该元素可能在堆上分配,也可能在栈上
        if sg.elem != nil {
            // 释放对应的内存
            typedmemclr(c.elemtype, sg.elem)
            sg.elem = nil
        }
        if sg.releasetime != 0 {
            sg.releasetime = cputicks()
        }

        // 将 goroutine 入 glist
        // 为最后将全部 goroutine 都 ready 做准备
        gp := sg.g
        gp.param = nil
        gp.schedlink.set(glist)
        glist = gp
    }

    
    // 将所有挂在 channel 上的 writer 从 sendq 中弹出
    // 该操作会使所有 writer panic
    for {
        sg := c.sendq.dequeue()
        if sg == nil {
            break
        }
        sg.elem = nil
        if sg.releasetime != 0 {
            sg.releasetime = cputicks()
        }

        // 将 goroutine 入 glist
        // 为最后将全部 goroutine 都 ready 做准备
        gp := sg.g
        gp.param = nil
        gp.schedlink.set(glist)
        glist = gp
    }

    // 在释放所有挂在 channel 上的读或写 sudog 时
    // 是一直在临界区的
    unlock(&c.lock)

    // Ready all Gs now that we've dropped the channel lock.
    for glist != nil {
        gp := glist
        glist = glist.schedlink.ptr()
        gp.schedlink = 0
        // 使 g 的状态切换到 Grunnable
        goready(gp, 3)
    }
}

可以从关闭的通道中读取数据吗?

可以,只有当返回值是false才是无效的。

通道关闭原则?

单一sender,由sender关闭。多个sender,通过channel来做广播信号关闭。

channel使得goroutine泄露?

channel长期处于满或者空,使得goroutine一直处在Gwaiting的状态。

读写情况总结

唯一可能panic的就是向关闭的channel中写数据,向nil中读写会被一直阻塞。

本文部分引用github.com/cch123/golang-notes的内容。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值