Channel底层原理


前言:说channel不得不说一下这个并发谚语经典:
Don’t communicate by sharing memory; share memory by communicating.
不要通过共享内存来通信,而应该通过通信来共享内存。这是作为 Go 语言的主要创造者之一的Rob Pike的至理名言,这也充分体现了Go言最重要的编程理念。而Channel恰恰是后半句话的完美实现,我们可以利用通道在多个GoRoutine之间传递数据。(这也是Channel最重要的使用场景)
这句话常想常新

1. channel 的概念

channel 是一个通道,用于端到端的数据传输,这有点像我们平常使用的消息队列,只不过 channel 的发送方和接受方是 goroutine 对象,属于内存级别的通信。
这里涉及到了 goroutine 概念,goroutine 是轻量级的协程,有属于自己的栈空间。 我们可以把它理解为线程,只不过 goroutine 的性能开销很小,并且在用户态上实现了属于自己的调度模型。
传统的线程通信有很多方式,像内存共享、信号量等。其中内存共享实现较为简单,只需要对变量进行并发控制,加锁即可。但这种在后续业务逐渐复杂时,将很难维护,耦合性也比较强。
后来提出了 CSP 模型,即在通信双方抽象出中间层,数据的流转由中间层来控制,通信双方只负责数据的发送和接收,从而实现了数据的共享,这就是所谓的通过通信来共享内存。 channel 就是按这个模型来实现的。
channel 在多并发操作里是属于协程安全的,并且遵循了 FIFO 特性。即先执行读取的 goroutine 会先获取到数据,先发送数据的 goroutine 会先输入数据。
另外,channel 的使用将会引起 Go runtime 的调度调用,会有阻塞和唤起 goroutine 的情况产生。

和其他结构(切片,map,结构体)相比的优势:并发安全

2. 通道类型及其使用

2.1. 非缓冲通道与缓冲通道

首先channel是一个单向通道,明确这一点后在看下面
非缓冲通道
var ch = make(chan int) // 声明一个非缓冲 channel
非缓冲通道,通常是用来进行同步数据操作,日常开发中用到的地方不多。
缓冲通道

  1. 带缓冲的 Channel 声明:带缓冲的 channel 可以在一定容量内缓存数据,只有当缓冲区满时发送操作才会阻塞。
    var ch = make(chan int, 10) // 声明一个容量为 10 的整数类型的带缓冲 channel

2.2 Channel的基础使用

2.2.1Channel使用

channel 的正常读写
往一个 channel 发送数据,可以这样写:

ch := make(chan int)
ch <- 1
对应的读操作:

data <- ch
当我们不再使用 channel 的时候,可以对其进行关闭:

close(ch)
● 如果关闭后的 channel 没有数据可读取时,将得到零值,即对应类型的默认值。
● 为了能知道当前 channel 是否被关闭,可以使用下面的写法来判断。
Channel中的for用法
a. 如果chs中没有任何数据,那么程序会阻塞在For这一行,直到有数据进来为止。
b. 如果chs已经被关闭了,那么程序会把所有元素都遍历完,再跳出循环。
c. 如果chs是nil,那么程序会永远阻塞在For这一行,不会往下执行。
if v, ok := <-ch; !ok {
fmt.Println(“channel 已关闭,读取不到数据”)
}
● 还可以使用下面的写法不断的获取 channel 里的数据:

for data := range ch {
  // get data dosomething
 }

Channel中的Select用法
d. 可读可写
e. select调度器
f. 在写程序时,有时并不单单只会和一个 goroutine 通信,当我们要进行多 goroutine 通信时,则会使用 select 写法来管理多个 channel 的通信数据:

ch1 := make(chan int)
ch2 := make(chan int)

go func() {
if !ok { // 某些原因,设置 ch1 为 nil
 ch1 = nil
}
}()

for {
select {
case <-ch1: // 当 ch1 被设置为 nil 后,将不会到达此分支了。
 doSomething1()
case <-ch2:
 doSomething2()
}
}


ch1 := make(chan struct{})
ch2 := make(chan struct{})

// ch1, ch2 发送数据
go sendCh1(ch1)
go sendCh1(ch2)

// channel 数据接受处理
for {
select {
case <-ch1:
 doSomething1()
case <-ch2:
 doSomething2()
}
}

2.2.2 Channel使用问题

  1. 两个携程等待接收一个信道,向信道传一个值的话谁获得这个值?
    a. 先来先得

读操作
我们以取一个元素为例,当我们操作Channel的时候实际上有这几个步骤:
找到元素->取元素的副本->将副本交给接收方->删除Channel中这个元素 我们最终能收到的就是这一串操作要么成功,要么失败,不会有中间态。这是Channel是并发安全的一个体现。
使用 channel 时我们还可以控制 channel 只读只写操作:

func readChan(ch <-chan int){
  // chan 只允许被读
 }

 func main(){
  ch := make(chan int)
  readChan(ch)
 }
反之,如果要求只写操作,则可以这样:

func writeChan(ch chan<- int){
     // chan 只允许被写
 }
 

3. 底层代码

3.1 底层结构

3.1.1hchsn结构体

type hchan struct {	
qcount uint//当前通道中的元素个数。	
dataqsiz uint//当前循环队列的长度。	
buf unsafe.Pointer //指向缓冲区的指针	环形数组的关键
elemsize uint16//通道元素的大小	信道元素类型所需内存的大小不同,
closed uint32//是否关闭	elemtype *_type //通道元素的类型	重复关闭会panic
sendx uint//发送操作处理到的位置	
recvx uint//接收操作处理到的位置	
recvq waitq //被阻塞的接收操作队列 链表结构	
sendq waitq //被阻塞的发送操作队列 链表结构
lock mutex //互斥锁
}

3.1.2 阻塞的携程队列链表

type waitq struct {
first *sudog //链表头节点
last *sudog //链表尾节点
}
阻塞携程元素
typw sudog struct {
g *g  //指向gorunting

next *sudog//下一个节点
prev *sudog//上一个节点
elem unsafe.Pointer//data element(may point to stack)

isSelect bool//代表着她所在的goruneting是否处于select多路复用的状态下

c *hchan//指向她所从属的channel
}

3.1.3 构造器函数

make构造两种信道的方法


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

	// compiler checks this but be safe.
	if elem.size >= 1<<16 {
		throw("makechan: invalid channel element type")
	}
	if hchanSize%maxAlign != 0 || elem.align > maxAlign {
		throw("makechan: bad alignment")
	}

	mem, overflow := math.MulUintptr(elem.size, uintptr(size))
	if overflow || mem > maxAlloc-hchanSize || size < 0 {
		panic(plainError("makechan: size out of range"))
	}

	
	var c *hchan
	switch {
	case mem == 0:
		// Queue or element size is zero.
		c = (*hchan)(mallocgc(hchanSize, nil, true))
		// Race detector uses this location for synchronization.
		c.buf = c.raceaddr()
	case elem.ptrdata == 0:
		// Elements do not contain pointers.
		// Allocate hchan and buf in one call.
		c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
		c.buf = add(unsafe.Pointer(c), hchanSize)
	default:
		// Elements contain pointers.
		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
}

3.2 发送send

两种异常:

3.2.1 case1:

if c == nil {//c是channel,为nil即没有正确初始化
		if !block {//block 参数用于指示是否要在当前操作被阻塞时等待,
//如果为 false,说明不希望被阻塞,直接返回 false 表示发送失败。
			return false
		}
		gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
//gopark 是 Go 语言运行时系统用来使 goroutine 进入休眠状态的函数。
		throw("unreachable")//抛出异常
	}//向一个没有初始话的channel传数据

var ch chan int//没有初始化的声明,直接send这种channel会让gorountine被动阻塞,导致死锁,并抛出一个异常阻塞

ch := make(chan int,10)//正常声明且初始化

3.2.2case2:

向一个关闭的channel进行send时会直接抛出一个错误

3.2.3具体chansend代码

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
//c信道
//ep带send的数据指针,
//gorountine是否可以阻塞
	if c == nil {
		if !block {
			return false
		}
		gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
		throw("unreachable")//抛出异常
	}//向一个没有初始话的channel传数据

	if debugChan {//是否在调试
		print("chansend: chan=", c, "\n")
	}//打印channel的地址

	if raceenabled {
		racereadpc(c.raceaddr(), callerpc, abi.FuncPCABIInternal(chansend))
	}
//如果 raceenabled 为真,表示编译时开启了数据竞争检测(Race Detector),
//则会调用 racereadpc 函数。
//Race Detector 用于检测并报告多个 goroutine 之间的数据竞争情况。
//racereadpc 用于标记一个读取操作的位置,
//以便 Race Detector 可以检测读取操作是否与其他写入操作存在竞争。
//这里会标记当前 channel 的地址和发送操作的位置。

if !block && c.closed == 0 && full(c) {//send的rountine不允许阻塞,且通道没有关闭,且通道已满
		return false
	}

	var t0 int64
	if blockprofilerate > 0 {
		t0 = cputicks()
	}
//当 blockprofilerate 大于 0 时,
//代码执行 cputicks() 并将返回的时钟周期数赋值给变量 t0。
//这就记录了当前的时间作为发送操作的起始时间。进行阻塞对于性能的分析

	
	lock(&c.lock)//只能有一个gorountine进行send操作

	if c.closed != 0 {
		unlock(&c.lock)
		panic(plainError("send on closed channel"))
	}//channel被关闭直接抛出错误


	if sg := c.recvq.dequeue(); sg != nil {//如果有队列
		send(c, sg, ep, func() { unlock(&c.lock) }, 3)//发送
		return true
	}
//c:信道
//crevq:等待接收的携程队列
//dequeue()取出第一个携程


//qcount缓冲区内数据数量
//dataqsiz缓冲区总大小
	if c.qcount < c.dataqsiz {//channel的缓冲区没有满
		qp := chanbuf(c, c.sendx)//获取缓冲区的send位置
		if raceenabled {  
			racenotify(c, c.sendx, nil)
		}//数据竞争有关
		typedmemmove(c.elemtype, qp, ep)//将数据放进channel中qp的位置
		c.sendx++//send位置++
		if c.sendx == c.dataqsiz {
			c.sendx = 0//如果send指针到尾部,就重置为0
		}
		c.qcount++//增加缓冲区数据计数
		unlock(&c.lock)
		return true
	}

	if !block {//不阻塞
		unlock(&c.lock)
		return false
	}

	gp := getg()//获取当前正在执行的 goroutine 的 g 结构体
	mysg := acquireSudog()//使用 acquireSudog() 函数获取一个空闲的 sudog 结构体,
//用于表示一个等待操作的 goroutine。sudog 是 Go 语言运行时系统用来管理等待和唤醒的结构
	mysg.releasetime = 0//首先,将 mysg 的 releasetime 字段设置为 0,表示释放时间尚未记录。
	if t0 != 0 {
		mysg.releasetime = -1
	}//不重点讲
	mysg.elem = ep//获取数据指针
	mysg.waitlink = nil//这个字段用于构建等待队列的链表结构。
	mysg.g = gp//将当前 goroutine(gp)赋值给 mysg 的 g 字段,
//表示这个 sudog 结构关联到当前的 goroutine。
	mysg.isSelect = false//表示这个 sudog 结构是用于等待发送操作。
	mysg.c = c//获取当前channel

//设置gp携程
	gp.waiting = mysg//表示gorounte正在等待
	gp.param = nil//用于清空可能存储的参数???
	c.sendq.enqueue(mysg)//将sudog添加在信道的发送阻塞队列
	gp.parkingOnChan.Store(true)//将当前 goroutine 的状态标记为正在阻塞在 channel 上。
	gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
	//让gorountine休眠

	KeepAlive(ep)//保持 ep 存活,以确保在发送操作完成后数据不会被提前回收。
	if mysg != gp.waiting {//
		throw("G waiting list is corrupted")
	}//检查等待的 sudog 结构是否与当前 goroutine 的 waiting 字段相符,
//如果不相符,说明等待列表被损坏,抛出异常
	gp.waiting = nil//如果为nil不再等待
	gp.activeStackChans = false//表示当前 goroutine 不再处于等待 channel 的状态。
	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
}

3.3.1 接收recv

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
   //c 通道
  //ep ep 是接收数据的目标地址
	//block 表示是否阻塞等待接收值
//selected  表示通道接收操作是否成功。
//如果 select 为 true,则表示成功进行了通道接收操作,不论是否实际接收到了一个值。
//如果 select 为 false,则表示通道接收操作失败,可能是因为通道已关闭或者非阻塞模式下通道为空。
//received 表示是否实际接收到了一个值。
//如果 received 为 true,则表示成功接收到了一个值。
//如果 received 为 false,则表示虽然成功进行了通道接收操作,但没有实际接收到一个值,
//通常是因为接收地址为 nil 或者通道中的数据类型不匹配等原因。
//selected 和 received 的默认值为 false
   if debugChan {//测试模式下使用
      print("chanrecv: chan=", c, "\n")
   }

   if c == nil {
      if !block {
         return
      }//可以不用导致异常
      gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
      throw("unreachable")
   }

   if !block && empty(c) {//gorune不能阻塞且通道为空
       if atomic.Load(&c.closed) == 0 {//通道关闭的话
        
         return//直接返回
      }
     if empty(c) {//它再次检查通道 c 是否为空。如果通道为空,这表示在上面的检查中通道状态没有发生变化,仍然为空。
     
         if raceenabled {//数据竞争检查不重要
            raceacquire(c.raceaddr())
         }
         if ep != nil {
            typedmemclr(c.elemtype, ep)//使ep为nil
         }
         return true, false//表示接收操作完成,但是没有实际接收到值
      }
   }

   var t0 int64//用以性能分析
   if blockprofilerate > 0 {
      t0 = cputicks()
   }

   lock(&c.lock)//上锁

   if c.closed != 0 {//通道关闭的话
      if c.qcount == 0 {//且缓存没有数据的话
         if raceenabled {//数据竞争
            raceacquire(c.raceaddr())
         }
         unlock(&c.lock)//解锁
         if ep != nil {//让ep为nil
            typedmemclr(c.elemtype, ep)
         }
         return true, false//表示接收操作完成,但是没有实际接收到值
      }
      
   } else {//通道没有关闭的话
     
      if sg := c.sendq.dequeue(); sg != nil {//如果发送队列不为nil
      
         recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
         return true, true
      }
//使用 recv 函数来完成接收。
//它将通道 c、发送操作 sg、接收地址 ep 以及一个解锁函数作为参数传递给 recv 函数。
//接收操作会尝试从发送操作中获取值并将其写入接收地址 ep
   }
//dequeue 方法会尝试从发送队列中获取一个发送操作

   if c.qcount > 0 {//缓冲区内有数据
   
      qp := chanbuf(c, c.recvx)//获取缓冲区的数值
      if raceenabled {//数据经常不重要
         racenotify(c, c.recvx, nil)
      }
      if ep != nil {
         typedmemmove(c.elemtype, ep, qp)
//则将缓冲区中的数据移动到接收地址 ep 中。这表示成功接收到了数据,并将其存储到指定的接收地址。
      }
      typedmemclr(c.elemtype, qp)
//清除缓冲区中的数据,因为数据已经被成功接收。
      c.recvx++//接收指针+1
      if c.recvx == c.dataqsiz {
         c.recvx = 0
      }//满则重置
      c.qcount--//缓存区数量-1
      unlock(&c.lock)解锁
      return true, true
   }//如果ep = nil,也会清楚qp在缓存区的数据

   if !block {//不能阻塞则返回
      unlock(&c.lock)
      return false, false
   }

 //下面的全部都是阻塞
   gp := getg()
   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
   c.recvq.enqueue(mysg)
  
   gp.parkingOnChan.Store(true)
   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
}

3.3.2具体完成的接收操作


func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
//c channel
//sg 携程
//ep 等待接收的指针
//unlockf 解锁函数
//skip 传的定值3
   if c.dataqsiz == 0 {//如果为 0,表示通道是非缓冲通道
      if raceenabled {//竞态检测,不重要
         racesync(c, sg)
      }
      if ep != nil {
         // copy data from sender
         recvDirect(c.elemtype, sg, ep)//从发送方(sg)复制数据到接收方(ep)
      }
   } else {
      // Queue is full. Take the item at the
      // head of the queue. Make the sender enqueue
      // its item at the tail of the queue. Since the
      // queue is full, those are both the same slot.
      qp := chanbuf(c, c.recvx)//它获取通道缓冲中的头部元素qp
      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)//从队列中复制数据到接收方(ep)。
      }
      // 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)
   sg.success = true//将 sg.success 设置为 true,表示接收成功。
   if sg.releasetime != 0 {
      sg.releasetime = cputicks()
   }
   goready(gp, skip+1)
}

3.4关闭close

func closechan(c *hchan) {
   if c == nil {
      panic(plainError("close of nil channel"))
   }//为nil直接panic

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

   c.closed = 1//表示已经关闭,相当于伪关闭

   var glist gList//用于存储等待读取或写入通道的 goroutine。

   // release all readers
   for {
      sg := c.recvq.dequeue()//释放所有等待接收的goroutinr
      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()//等待写入的goroutine
      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)解锁

   // Ready all Gs now that we've dropped the channel lock.
   for !glist.empty() {
      gp := glist.pop()//将goroutine取出
      gp.schedlink = 0//这里将其设置为 0,表示不再具有与调度相关的链接。
      goready(gp, 3)
   }//将 gp 标记为可调度状态,并将其放入调度队列中以等待执行。
//3 是一个可选的参数,可能用于指示在调度时的优先级或其他调度相关信息。
}

4. 情景案例

4.1 close部分

关闭一个有发送阻塞的channel会直接报错
关闭一个有接收阻塞的channel会解开阻塞,并且接收的值为零值
channel 的 deadlock
前面提到过,往 channel 里读写数据时是有可能被阻塞住的,一旦被阻塞,则需要其他的 goroutine 执行对应的读写操作,才能解除阻塞状态。
然而,阻塞后一直没能发生调度行为,没有可用的 goroutine 可执行,则会一直卡在这个地方,程序就失去执行意义了。此时 Go 就会报 deadlock 错误,如下代码:


func main() {
  ch := make(chan int)
  <-ch

  // 执行后将 panic:
  // fatal error: all goroutines are asleep - deadlock!
 }

因此,在使用 channel 时要注意 goroutine 的一发一取,避免 goroutine 永久阻塞!

4.2 阻塞与非阻塞模式

读取一个0值如何判断是本身传递的值就是0,还是关闭后为0

if val,ok := <- ch;ok {
//open
} else {
//close
}
//使用时
ch := make(chan int, 2)
got1 := <- ch       chanrecv1
got2,ok := <- ch		chanrecv2

//实现上述功能的原因是,两种格式下,读channel方法会被会变成不同的方法
func chanrecv1(c *hchan, elem unsage.Pointer) {
	chanrecv(c,elem,true)
}

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

4.3 读取一个已关闭的channel会发生什么

4.3.1 读一个已关闭的channel

如何缓存区有数值,则正常读取,如果缓存区没值或者没有缓冲区,则返回一个channel元素类型的零值和一个bool类型的false

4.3.2 写入一个已关闭的channel

直接报panic,写入一个已关闭的channel是非法的,

5.总结

最后提问:

  1. 如何设置携程的block
  2. 如何唤醒阻塞的goruntine(具体代码)
  3. 发送阻塞的数据进不进入信道
  4. 还有其他问题吗?

最后总结:
channel的问题并不难,主要的难点就是并发这一个,如何让携程阻塞的,唤醒的这一大块是难点,相比map,基础的channel还是非常简单的

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值