1.必须双方碰面才会携手往下走。

——无缓存通道的情形下,接收方(<-c1)和发送方(c1 <- true )会互相等待。

  • 也即:如果接收方走到 <-c1 而发送方没有走到c1 <- true,则接收方协程会阻塞,等待发送方执行完c1 <- true 双方协程才会共同继续。反之亦然。
  • 此时发送方和接收方的代码可以互换,不影响逻辑的进行。(但仍然建议由主协程或母协程来作为接收方)

2.塞满时发送方会等他腾出空位再往里塞。

——有缓存通道的情形下,如果缓存已满,会阻塞发送方的协程。

  • 直到有接收方取出信息(也即执行 <-c1 ),为后续信息腾出空位。

3.拿空后接收方会等他有数据了再继续拿。

——有缓存通道的情形下,如果缓存为空,会阻塞接收方的协程。

  • 直到有发送方存入信息(也即执行c1 <- true)。

相关代码如下

var c1 = make(chan bool)
func func1() {
	//...  
	c1 <- true  
	//...  
}

func main() {
	go func1()
	
	//...  
	<-c1
	//...  
	select {} //保证协程结束
}

扩展应用:用缓存通道控制并发数目

  • 本质是上文中 2. 的机制的运用
  • 在大量并发的子协程的入口处使用c1 <- true,利用上文中2. 的机制阻塞新协程的开启;并在执行完毕后执行 <-c1,腾出缓存中的空间,此时则会放行某一个阻塞中的c1 <- true完成执行,进入后续步骤。如此宏观上的表现即为通道c1中定义的缓存数量等于所允许的子协程并发量的上限。

示例代码:

package main

import (
	"time"
)

var limit = make(chan int, 3)

func main() {

	for i := 0; i < 20; i++ {

		limit <- 1 //缓存有空间,或接收方有就绪的,才会继续往下走
		println("循环次数--", i)

		go func(i int) { //同时也是协程持有的是引用的示例

			//println("func-", i, "阻塞中")
			//limit <- 1 //阻塞的代码也可以写在子协程生成后

			println("执行中--", i)
			time.Sleep(2 * time.Second)
			println("       ", i, "--执行完成")
			<-limit

			//也即,从传入到传出的中间这段,同时只能存在3个运行中,第四个的limit <- 1会等第一批的三个中有一个走到<-limit才能继续(也即释放了一个chan的缓存|缓存有了空间,可以接收新的传入)

		}(i)
	}

	select {}
}

观察结果可知,当缓存通道被占满后,每有一个“执行完成”,“循环次数”才会往下走一次。