GO学习PART1——channel

文章详细介绍了Go语言中的channel基本用法,包括初始化、读操作、写操作和关闭channel,并探讨了channel的内部数据结构和读写操作的实现原理。通过源码分析,揭示了channel如何确保线程安全和goroutine间的通信。
摘要由CSDN通过智能技术生成

GO学习PART1——channel

Channel

通道(channel)是Go中的一种特殊的类型,主要用于goroutine之间的消息传递,且channel是一种线程安全的结构

channel基本用法

初始化
// 一般采用make的方法去初始化channel
// 1.构造容量为3的int型channel
make(chan int,3)
// make(chan 数据类型,容量)
// 2.构造任意数据类型的channel
make(chan interface{},3)
// 3.构造没有容量的channel
make(chan struct{})
读操作
ch := make(chan int,3)
// 1.阻塞式接受数据,如果缓存中没有数据可以读,那么goroutine会阻塞
data := <- ch
// 2.非阻塞式接受,不会阻塞,一般是select语句进行使用
data,ok := <- ch
// 3.无视接受的数据,一般这种chan作为信号
<- ch
写操作
ch := make(chan int,3)
// 写入操作,如果缓存中的数据满了,那么会阻塞,直到缓冲中有空的位置,然后写入数据唤醒
ch <- 10
关闭
// 关闭channel
close(ch)

channel源码分析

channel的源码关联在runtime/chan.go,使用的Go SDK版本是1.20

channel数据结构

开篇就是channel的数据结构,存储的chan的数据结构是一种类似于环形数组的方式

type hchan struct {
   qcount   uint           // total data in the queue   缓冲区元素数量
   dataqsiz uint           // size of the circular queue  缓冲区的大小
   buf      unsafe.Pointer // points to an array of dataqsiz elements 缓冲区内存地址起点
   elemsize uint16	// 数据大小,用于内存地址分配的计算
   closed   uint32  // 判断channel是否被关闭
   elemtype *_type // element type 数据的类型
   sendx    uint   // send index 写数据的起始位置
   recvx    uint   // receive index 读数据的起始位置
   recvq    waitq  // list of recv waiters 读数据的routine阻塞队列
   sendq    waitq  // list of send waiters 写数据的routine阻塞队列

   // lock protects all fields in hchan, as well as several
   // fields in sudogs blocked on this channel.
   //
   // Do not change another G's status while holding this lock
   // (in particular, do not ready a G), as this can deadlock
   // with stack shrinking.
   lock mutex // 互斥锁,保证读写只有一个goroutine在操作
}

waitq、sudog类型

type waitq struct {
   first *sudog //一个双向列表的头指针
   last  *sudog //一个双向列表的尾指针
}
type sudog struct {
   // ...省略
   next *sudog //next指针
   prev *sudog //prev指针
   // ...省略
}

那么对于channel的数据结构其实已经非常的清楚了,他主要就是3个部分

  • 互斥锁,保证并发正确性
  • 环形数组
  • 读、写的阻塞队列
channel读写操作(以读操作为例)

读操作主要调用的函数chanrecv在源码的457行

首先解释block变量,他是对于是否是阻塞式的判断的变量,因为对于非阻塞式读取,不应该把当前线程阻塞,而是应该返回第二个变量参数为false

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
    // ...
    // 如果channel的地址为nil,说明channel没有初始化,那么也不会对他进行读操作
    // 首先判断是否是阻塞式,如果不是,那么直接返回值,如果是的话就会把当前协程挂起,抛出异常
	if c == nil {
		if !block {
			return
		}
		gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
		throw("unreachable")
	}
    
    //...
    // 后面操作要对数据进行操作,需要提前加锁
    lock(&c.lock)
    // 1. channel关闭:如果数组里面没有数据可以读了,那么就返回数据的默认值;如果还有数据就不做任何操作,后面继续读取数据
    // 2. channel没有关闭:如果写队列的队列头指针不为空,说明写队列有routine阻塞,这也就说明当前的缓冲区已经满了所以才有协程阻塞,因此会读取数据同时唤醒写协程区写一个数据,同时读取一个
    if c.closed != 0 {
		if c.qcount == 0 {
			if raceenabled {
				raceacquire(c.raceaddr())
			}
			unlock(&c.lock)
			if ep != nil {
				typedmemclr(c.elemtype, ep)
			}
			return true, false
		}
		// The channel has been closed, but the channel's buffer have data.
	} else {
		// Just found waiting sender with not closed.
		if sg := c.sendq.dequeue(); sg != nil {
			// Found a waiting sender. If buffer is size 0, receive value
			// directly from sender. Otherwise, receive from head of queue
			// and add sender's value to the tail of the queue (both map to
			// the same buffer slot because the queue is full).
			recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
			return true, true
		}
	}
    // 1.缓冲区有数据,那么会通过计算chan的下一个读的位置,将内存数据移动到指定的eq地址,同时recvx++,指向下一个读取位置;随后判断recvx是否越界,越界的话需要重置位置,这也是为什么说它是类环形结构的原因,他只是逻辑上是个环形。读取数据之后需要释放锁,防止死锁,然后返回值
    if c.qcount > 0 {
		// Receive directly from queue
		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
	}
	// 2.缓冲区没有数据,首先判断是否是阻塞读取,如果是非阻塞读取的话,那么需要直接返回
	if !block {
		unlock(&c.lock)
		return false, false
	}
    // 后续就是阻塞队列的创建,然后加入阻塞队列,在释放锁之后进行阻塞
    // 最后就是在唤醒的时候需要把写入数据进行读取
    // ...
}
channel关闭

channel 在关闭的时候需要唤醒所有阻塞的进程

func closechan(c *hchan) {
    // 判断当前的chan有没有初始化,不能关闭一个没有初始化的chan
   if c == nil {
      panic(plainError("close of nil channel"))
   }
	// 加锁,后续对于数据的操作
    // 判断有没有重复的关闭,如果已经关闭,那么抛出异常
   lock(&c.lock)
   if c.closed != 0 {
      unlock(&c.lock)
      panic(plainError("close of closed channel"))
   }

   if raceenabled {
      callerpc := getcallerpc()
      racewritepc(c.raceaddr(), callerpc, abi.FuncPCABIInternal(closechan))
      racerelease(c.raceaddr())
   }
	// 关闭channel
   c.closed = 1
	// 用于存储未处理的读协程或者写协程
   var glist gList
	//后续对于读/写协程的遍历,但是对于channel来说,不可能同时存在既有读协程和写协程,因此相当于下面两个队列只有一个队列存留(因为如果有读协程,那么说明缓冲区的数据是空的,那么写协程就不可能被阻塞,反之亦然)
   // release all readers
   for {
      sg := c.recvq.dequeue()
      if sg == nil {
         break
      }
      if sg.elem != nil {
         typedmemclr(c.elemtype, sg.elem)
         sg.elem = nil
      }
      if sg.releasetime != 0 {
         sg.releasetime = cputicks()
      }
      gp := sg.g
      gp.param = unsafe.Pointer(sg)
      sg.success = false
      if raceenabled {
         raceacquireg(gp, c.raceaddr())
      }
      glist.push(gp)
   }

   // release all writers (they will panic)
   for {
      sg := c.sendq.dequeue()
      if sg == nil {
         break
      }
      sg.elem = nil
      if sg.releasetime != 0 {
         sg.releasetime = cputicks()
      }
      gp := sg.g
      gp.param = unsafe.Pointer(sg)
      sg.success = false
      if raceenabled {
         raceacquireg(gp, c.raceaddr())
      }
      glist.push(gp)
   }
   unlock(&c.lock)
	// 对所有的阻塞协程进行goready操作,唤醒
   // Ready all Gs now that we've dropped the channel lock.
   for !glist.empty() {
      gp := glist.pop()
      gp.schedlink = 0
      goready(gp, 3)
   }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值