5. Channel

5. Channel

为什么使用管道而不使用共享内存:

  1. 避免协程竞争和数据冲突问题
  2. 降低开发难度,增加程序可读性
  3. 模块之间更容易解耦,增加扩展性和可维护性

5.1 Channel 底层数据结构

实现 Channel 大概需要如下几个部分:

image-20220630171342999
  1. 如果没有协程接收,需要一个队列记录正在发送的协程
  2. 如果没有协程发送,需要一个记录接收数据协程的队列
  3. 需要一个缓冲区,缓冲发送的数据

查看 Go 语言 Channel 的底层数据结构:runtime/chan.go

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
	elemtype *_type // element type
	sendx    uint   // send index
	recvx    uint   // receive index
	recvq    waitq  // list of recv waiters
	sendq    waitq  // list of send waiters

	// 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  // 用于保护hchan中的字段,任何协程想要操作该结构体都需要获取这把锁
}
  • 其中下面的这些字段组成了一个环形缓存区
qcount   uint           // 环形队列中元素的数量
dataqsiz uint           // 环形缓冲区大小
buf      unsafe.Pointer // 指向环形缓冲区中的元素
elemsize uint16         // 元素大小
elemtype *_type         // 元素类型
image-20220630173811263
  • 环形缓存可以大幅度降低 GC 的开销
    • 如果使用队列,不断新增/删除数据需要开辟/回收内存空间,这会造成 GC 压力大
    • 如果使用环形缓存,永远占用同一片内存,有新的数据来的时候,直接放在下一个块上就可以了

发送和接收队列有下面四个属性组成

sendx    uint   // send index
recvx    uint   // receive index
recvq    waitq  // list of recv waiters 等待接收队列
sendq    waitq  // list of send waiters 等待发送队列
  • 这里没有使用 sema,而是使用链表实现发送队列和接收队列

  • 查看 waitq 结构体,sudog 前面已经介绍过了,里面会存放一个协程,这是一个以 sudog 为节点的双向链表

type waitq struct {
	first *sudog  // 指向链表第一个成员
	last  *sudog  // 指向链表最后一个成员
}
image-20220721163618667

注意:hchan 结构体的 lock 是一个 Mutex,该锁并不是用来排队发送/接收数据的

  • 这里的互斥锁用来保护 hchan 结构体的所有字段的,任何协程想要操作 hchan 结构体中的变量,就需要加 Mutex
  • 就像前面讲解的员工结构体,如果想要修改员工结构体中的数据,就需要加锁
  • Channel 本身并不是无锁的,他带有一个 Mutex,所以说 Channel 是一个无锁的并不完全正确
  • Channel 快是因为我们只是在往 Channel 中塞数据的时候需要加锁,塞完数据后就可以解锁了,不需要一直加锁

补充:hchan 结构体中还有一个 closed 字段,0 表示开启,1 表示关闭

总结:Channel 是 go 的一等公民,大致结构体如下:
image-20220630175038544

5.2 Channel 发送数据

在编译期间,会将 ch <- 操作转换为 runtime.chansend1() 方法,如下:

// entry point for c <- x from compiled code
//go:nosplit
func chansend1(c *hchan, elem unsafe.Pointer) {
	chansend(c, elem, true, getcallerpc())
}
  • chansend1 中调用 chansend 方法,查看 chansend 方法
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
	if c == nil {
		if !block {
			return false
		}
		gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
		throw("unreachable")
	}

	if debugChan {
		print("chansend: chan=", c, "\n")
	}

	if raceenabled {
		racereadpc(c.raceaddr(), callerpc, funcPC(chansend))
	}

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

	if sg := c.recvq.dequeue(); sg != nil {
		// Found a waiting receiver. We pass the value we want to send
		// directly to the receiver, bypassing the channel buffer (if any).
		send(c, sg, ep, func() { unlock(&c.lock) }, 3)
		return true
	}

	if c.qcount < c.dataqsiz {
		// Space is available in the channel buffer. Enqueue the element to send.
		qp := chanbuf(c, c.sendx)
		if raceenabled {
			racenotify(c, c.sendx, nil)
		}
		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
	}

	// Block on the channel. Some receiver will complete our operation for us.
	gp := getg()
	mysg := acquireSudog()
	mysg.releasetime = 0
	if t0 != 0 {
		mysg.releasetime = -1
	}
	// No stack splits between assigning elem and enqueuing mysg
	// on gp.waiting where copystack can find it.
	mysg.elem = ep
	mysg.waitlink = nil
	mysg.g = gp
	mysg.isSelect = false
	mysg.c = c
	gp.waiting = mysg
	gp.param = nil
	c.sendq.enqueue(mysg)
	atomic.Store8(&gp.parkingOnChan, 1)
	gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
	KeepAlive(ep)

	// someone woke us up.
	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
}

Channel 发送分为三种情形:

  1. 直接发送
  2. 放入缓存
  3. 休眠等待

5.2.1 直接发送

直接发送:

  • 发送之前已经有 G 在休眠等待接收数据了
  • 此时缓存肯定是空的,不用考虑缓存
  • 将数据直接拷贝给 G 的接收变量,唤醒 G(不需要将数据存放到缓存区中
image-20220702095613592

直接发送实现步骤:

  1. 从接收队列中取出一个等待接收的 G
  2. 将数据直接拷贝到接收变量中
  3. 唤醒 G
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
	...

	lock(&c.lock)  // 往channel中添加数据,需要修改hchan结构体,需要上锁

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

    // 1. 从等待队列中取一个协程,并判断拿出来的是否是nil,如果不是nil,表示有协程在等待接收数据
	if sg := c.recvq.dequeue(); sg != nil {  
		// Found a waiting receiver. We pass the value we want to send
		// directly to the receiver, bypassing the channel buffer (if any).
        // 2. sg不是nil表示有协程在等待,执行send方法,参数(channel, 等待接收的协程的sudog, s)
		send(c, sg, ep, func() { unlock(&c.lock) }, 3)
		return true
	}
	...
}

func lock(l *mutex) {
	lockWithRank(l, getLockRank(l))
}
  • 查看 send 方法
func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
	...
    // 1. sg 是真正等待的协程的包装,sg.elem 是协程存放接收的数据的地方
    // 例如 i := <-c,就会将 i 这个变量的指针放在 elem 中(指向了要用哪一个变量来接收channel中的数据)
    // 如果g中的数据不为空,执行如下逻辑
	if sg.elem != nil {
        // 执行 sendDirect 方法,参数(chan中数据的类型, 正在等待的协程sudog, 数据的指针)
		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()
	}
	goready(gp, skip+1)  // 数据接收完成后唤醒等待的协程,和goproc相反
}
  • 查看 sendDirect 方法
// 参数(chan中数据的类型, 正在等待的协程sudog, 数据的指针)
func sendDirect(t *_type, sg *sudog, src unsafe.Pointer) {
	// src is on our stack, dst is a slot on another stack.

	// Once we read sg.elem out of sg, it will no longer
	// be updated if the destination's stack gets copied (shrunk).
	// So make sure that no preemption points can happen between read & use.
	dst := sg.elem  // 目的地的指针(接收者需要将数据存放到哪里)
	typeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.size)
	// No need for cgo write barrier checks because dst is always
	// Go memory.
	memmove(dst, src, t.size)  // 将要发送来的数据src直接移动给了要接收的协程的变量dst
}
  • 运行完 sendDirect 后,会运行 goready 唤醒等待的协程
func goready(gp *g, traceskip int) {
	systemstack(func() {
		ready(gp, traceskip, true)
	})
}

5.2.2 放入缓存

放入缓存:没有 G 在休眠等待,但是有缓存空间

image-20220702102748151

放入缓存实现步骤:

  1. 获取可存入的缓存地址
  2. 直接将数据存放到缓存中
  3. 维护缓存的索引
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
	...
    // 判断是否可以从等待队列中获取协程
    ...

    // 判断缓存区是否还可以存放数据(缓存数量 < 最多能缓存的个数)
	if c.qcount < c.dataqsiz {
		// Space is available in the channel buffer. Enqueue the element to send.
        // 1. 从环形缓存中拿出一个可以用的缓存单元
		qp := chanbuf(c, c.sendx)
		if raceenabled {
			racenotify(c, c.sendx, nil)
		}
        // 2. 将数据存放到该缓存单元中
		typedmemmove(c.elemtype, qp, ep)
        // 3. 维护环形缓存的索引
		c.sendx++
		if c.sendx == c.dataqsiz {
			c.sendx = 0
		}
		c.qcount++
		unlock(&c.lock)
		return true
	}

	...
}

总结:放入缓存的逻辑不需要与其他任何的协程有关联,只需要判断缓存区是否还有空间并存放到缓存中即可,如果缓存满了,会进入休眠等待

5.2.3 休眠等待

休眠等待:

  • 没有G休眠等待,并且缓存区满了或没有缓存区
  • 协程自己进入发送队列,休眠等待
image-20220702103804149

休眠等待实现步骤:

  1. 将自己包装成 sudog
  2. 将 sudog 存放到 sendq 队列
  3. 休眠并解锁(在操作channel中的数据的时候,需要加 Mutex 锁,协程操作channel后进入休眠状态,将 Mutex 解锁)
  4. 注意:当休眠的协程被唤醒时,数据已经被取走了,需要维护其他数据,例如:索引....
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
    // 缓存区已经满了,执行下面的逻辑
	...
	// Block on the channel. Some receiver will complete our operation for us.
	gp := getg()  // 1. 拿到自己的协程结构体
	mysg := acquireSudog()  // 2. 将自己包装成sudog
	mysg.releasetime = 0
	if t0 != 0 {
		mysg.releasetime = -1
	}
	// No stack splits between assigning elem and enqueuing mysg
	// on gp.waiting where copystack can find it.
	mysg.elem = ep  // 3. 自己要发送的数据,记录在elem中
	mysg.waitlink = nil
	mysg.g = gp     // 4. 自己的协程的指针
	mysg.isSelect = false
	mysg.c = c
	gp.waiting = mysg
	gp.param = nil
	c.sendq.enqueue(mysg)  // 5. 将自己存放到sendq休眠队列中

	atomic.Store8(&gp.parkingOnChan, 1)
    // 6. 休眠,协程会卡在这句话
	gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
    
    // 7. 被唤醒后执行下面的逻辑

	...
	return true
}

Channel 数据发送总结:

  1. 编译阶段,会将 ch <- 操作转换为 runtime.chansend1() 方法
  2. 直接发送时,将数据直接拷贝给目标变量
  3. 放入缓存时,将数据存放到环形缓存中,成功返回
  4. 休眠等待时,将自己包装成 sudog 并存放到 sendq 中,休眠

5.3 Channel 数据接收

Go 语言中 <- ch 表示将数据从 channel 中取出来

  • 该操作会在编译的时候将 i := <- ch 转换为 runtime.chanrecv1() 方法

  • 如果使用两个参数接收:i, ok := <- ch 会将其转换为 runtime.chanrecv2() 方法

  • 最终两个方法都会调用 chanrecv 方法

// entry points for <- c from compiled code
//go:nosplit
func chanrecv1(c *hchan, elem unsafe.Pointer) {
	chanrecv(c, elem, true)
}

//go:nosplit
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) {
	// raceenabled: don't need to check ep, as it is always on the stack
	// or is new memory allocated by reflect.

	if debugChan {
		print("chanrecv: chan=", c, "\n")
	}

	if c == nil {
		if !block {
			return
		}
		gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
		throw("unreachable")
	}

	// Fast path: check for failed non-blocking operation without acquiring the lock.
	if !block && empty(c) {
		if atomic.Load(&c.closed) == 0 {
			return
		}
		if empty(c) {
			// The channel is irreversibly closed and empty.
			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)

	if c.closed != 0 && c.qcount == 0 {
		if raceenabled {
			raceacquire(c.raceaddr())
		}
		unlock(&c.lock)
		if ep != nil {
			typedmemclr(c.elemtype, ep)
		}
		return true, false
	}

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

	// no sender available: block on this channel.
	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)
	atomic.Store8(&gp.parkingOnChan, 1)
	gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)

	// someone woke us up
	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
}

数据接收时有四种情况:

  1. 有等待的 g,从 g 接收
  2. 有等待的 g,从缓存中接收
  3. 接收缓存
  4. 阻塞接收

5.3.1 有等待的 g,从 g 接收

有等待的 g,从 g 接收

  • 接收数据前,已经有 g 在休眠等待发送
  • Channel 中没有缓存
  • 将数据直接从 g 中拷贝出来,并唤醒 g
image-20220702105824964

有等待的 g,从 g 接收实现步骤:

  1. 判断是否有 g 在发送队列中等待
  2. 判断 channel 是否有缓存
  3. 直接从等待的 g 中取走数据,唤醒 g
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
    
	...

	lock(&c.lock)
    
	// 判断channel是否已经关闭了
	if c.closed != 0 && c.qcount == 0 {
		if raceenabled {
			raceacquire(c.raceaddr())
		}
		unlock(&c.lock)
		if ep != nil {
			typedmemclr(c.elemtype, ep)
		}
		return true, false
	}

    // 从发送队列中拿出一个协程,如果拿出来的不是空,则表示发送队列中有协程休眠
	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方法
		recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
		return true, true
	}

	...
	return true, success
}
  • 查看 recv 方法
func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
    // 如果没有缓存或者缓存区为空,执行下面的逻辑
	if c.dataqsiz == 0 {
		if raceenabled {
			racesync(c, sg)
		}
		if ep != nil {
			// copy data from sender
            // 执行recvDirect方法
			recvDirect(c.elemtype, sg, ep)
		}
	} else {
		...
	}
    // 维护协程状态
	sg.elem = nil
	gp := sg.g
	unlockf()
	gp.param = unsafe.Pointer(sg)
	sg.success = true
	if sg.releasetime != 0 {
		sg.releasetime = cputicks()
	}
    // 唤醒等待发送的协程
	goready(gp, skip+1)
}
  • 查看 recvDirect() 方法
func recvDirect(t *_type, sg *sudog, dst unsafe.Pointer) {
	// dst is on our stack or the heap, src is on another stack.
	// The channel is locked, so src will not move during this
	// operation.
	src := sg.elem
	typeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.size)
    // 直接将数据移动到目标地址即可
	memmove(dst, src, t.size)
}

5.3.2 有等待的g,从缓存接收

有等待的g,从缓存接收:

  • 接收数据前,已经有 G 在休眠等待发送
  • 而且这个 Channel 有缓存
  • 从缓存取走一个数据
  • 将休眠 G 的数据放进缓存,唤醒 G
image-20220702111025089

有等待的g,从缓存接收实现步骤:

  1. 判断有 G 在发送队列等待,进入 recv()
  2. 判断此 Channel 有缓存
  3. 从缓存中取走一个数据
  4. 将 G 的数据放入缓存,唤醒 G
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
	...
	// 1. 判断是否有协程在休眠等待
	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).
        // 2. 执行 recv 方法
		recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
		return true, true
	}

	...
}
  • 执行 recv 方法
func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
    // 1. 判断是否有缓存,有则执行else
	if c.dataqsiz == 0 {
		...
	} else {
        // 2. 从缓存中取出一个数据
		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 {
            // 3. 将取出来的数据直接拷贝给接收g的elem中
			typedmemmove(c.elemtype, ep, qp)
		}
		// copy data from sender to queue
        // 4. 将等待的协程的数据拷贝到缓存中
		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
	}
	...
    // 唤醒等待的协程
	goready(gp, skip+1)
}

5.3.3 接收缓存

接收缓存:没有协程等待,且缓存中有数据

image-20220702112223666

接收缓存实现步骤:

  1. 判断队列中是否有协程等待
  2. 判断 Channel 中是否有缓存数据
  3. 从缓存中取出数据
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
	...
    // 缓存中有数据
	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
	}

	...
}

5.3.4 阻塞接收

阻塞接收:

  • 没有 G 在休眠等待,而且没有缓存或缓存空
  • 自己进入接收队列,休眠等待
image-20220702112245647

阻塞接收实现步骤:

  1. 判断没有 G 在发送队列等待
  2. 判断此 Channel 无缓存
  3. 将自己包装成 sudog
  4. sudog 放入接收等待队列,休眠.
  5. 唤醒时,发送的 G 已经把数据拷贝到位
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
	...
    // 无法拿到数据,走如下逻辑
	// no sender available: block on this channel.
    // 1. 拿到自己的g结构体
	gp := getg()
	mysg := acquireSudog()  // 2. 包装为sudog
	mysg.releasetime = 0
	if t0 != 0 {
		mysg.releasetime = -1
	}
	// No stack splits between assigning elem and enqueuing mysg
	// on gp.waiting where copystack can find it.
	mysg.elem = ep
	mysg.waitlink = nil
	gp.waiting = mysg
	mysg.g = gp
	mysg.isSelect = false
	mysg.c = c
	gp.param = nil
    // 3. 将sudog存放到接收等待队列中
	c.recvq.enqueue(mysg)
	// Signal to anyone trying to shrink our stack that we're about
	// to park on a channel. The window between when this G's status
	// changes and when we set gp.activeStackChans is not safe for
	// stack shrinking.
	atomic.Store8(&gp.parkingOnChan, 1)
    // 4. 休眠
	gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)

    // 5. 唤醒的时候,不需要考虑数据的问题,数据已经被发送的协程塞给他了,对应发送数据的直接发送
	// someone woke us up
	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
}

Channel 数据接收总结:

  • 编译阶段,<-ch 会转化为 chanrecv()
  • 有等待的 G,且无缓存时,从 G 接收
  • 有等待的 G,且有缓存时,从缓存接收
  • 无等待的 G,且缓存有数据,从缓存接收
  • 无等待的 G,且缓存无数据,等待喂数据

5.4 实现非阻塞 channel

5.4.1 select 底层实现

前面我们查看源码的时候,在接收数据的时候,可以传递非阻塞的标志(chanrecv 方法的 block 参数),该参数为 false 表示非阻塞的 channel

当我们使用 select 关键字的时候,传递的参数为 false

func main() {
	ch1 := make(chan int, 5)
	ch2 := make(chan int)
	select {
	case <-ch1:  // 如果可以从ch1中取数据,执行下面的逻辑
		fmt.Println("ch1 recv")
	case ch2 <- 1:  // 如果可以往ch2中发送数据,执行下面的逻辑
		fmt.Println("ch2 recv")
	default:  // 上面的逻辑都走不通,执行default
		fmt.Println("none")
	}
}

select 的原理:同时存在接收、发送、默认路径时

  1. 查看是否存在可以即时执行的 case
  2. 没有则走 default
  3. 如果没有 default,则会将 select 注册到 ch1 的接收队列,ch2 的发送队列中,然后 select 休眠
  4. 当 ch1 和 ch2 中有一个唤醒 select,就会走对应的逻辑

5.4.2 Timer

Timer 提供了一个工具,可以定时的塞入数据,如下:

func main() {
	t := time.NewTimer(time.Second)
	<-t.C     // 会在倒计时结束后往channel中塞入一个数据,然后我们再将该数据取出来,x
	fmt.Println("hello")
}
posted @ 2022-09-07 11:18  Reed-Luo  阅读(65)  评论(0)    收藏  举报