如果在初始化一个通道时将其容量设置成0,或者直接忽略对容量的设置,就会使该通道成为一个非缓冲通道。
与以异步的方式传递元素值的缓冲通道不同,
非缓冲通道只能同步地传递元素值。
happens before
-
happens before与缓冲通道相比,针对非缓冲通道的 happens before原则有两个特别之处,具体如下。
-
向此类通道发送元素值的操作会被阻塞,直到至少有一个针对该通道的接收操作进行为止。
-
发送:先接收后发送
该接收操作会先得到元素值的副本,然后在唤醒发送方所在的goroutine之后返回。也就是说,这时的接收操作会在对应的发送操作完成之前完成。 -
接收:先发送后接收
从此类通道接收元素值的操作会被阻塞,直到至少有一个针对该通道的发送操作进行为止。该发送操作会直接把元素值复制给接收方,然后在唤醒接收方所在的goroutine之后返回。也就是说,这时的发送操作会在对应的接收操作完成之前完成。
这两条规则都是源码级的。由于Go运行时系统的实时调度,你不一定能从程序的表象(比如输出)上验证它们。还是那句话,不要被表象迷惑。
请先牢记,只有在针对 非缓冲通道的发送方和接收方“握手”之后,元素值的传递才会进行,然后双方的操作才能完成。另外,如果发送方或/和接收方有多个,它们就需要排队“握手”。
同步特性
单向的非缓冲通道在使用上并没有什么特别之处。非缓冲通道在与 for 语句或select语句连用时,也与用缓冲通道一般无二。
不过,由于非缓冲通道会以同步的方式传递元素值,在其上收发元素值的速度总是与慢的那一方持平。
package main
import (
"fmt"
"time"
)
func main() {
sendingInterval := time.Second *4
receptionInterval := time.Second * 2
intChan := make(chan int, 0)
go func() {
var ts0, ts1 int64
for i := 1; i <= 5; i++ {
intChan <- i
ts1 = time.Now().Unix()
if ts0 == 0 {
fmt.Println("Sent:", i)
} else {
fmt.Printf("Sent: %d [interval: %d s]\n", i, ts1-ts0)
}
ts0 = time.Now().Unix()
time.Sleep(sendingInterval)
}
close(intChan)
}()
var ts0, ts1 int64
Loop:
for {
select {
case v, ok := <-intChan:
if !ok {
break Loop
}
ts1 = time.Now().Unix()
if ts0 == 0 {
fmt.Println("Received:", v)
} else {
fmt.Printf("Received: %d [interval: %d s]\n", v, ts1-ts0)
}
}
ts0 = time.Now().Unix()
time.Sleep(receptionInterval)
}
fmt.Println("End.")
}
// 输出
Sent: 1
Received: 1
Sent: 2 [interval: 4 s]
Received: 2 [interval: 4 s]
Sent: 3 [interval: 4 s]
Received: 3 [interval: 4 s]
Sent: 4 [interval: 4 s]
Received: 4 [interval: 4 s]
Received: 5 [interval: 4 s]
Sent: 5 [interval: 4 s]
End.
可以看到,发送操作和接收操作的间隔时间都与receptionInterval变量的值一致。如果你把sendingInterval变量的值改为time.Second * 4,那么运行程序后的打印内容就会显示发送操作和接收操作的间隔时间都变成了4s,如此就验证了前文所述的表象。
如果你再把通道的声明语句改为intChan := make(chan int, 5),那么运行程序后又会看到另一番景象。我想你一定可以解释为什么打印的内容又会不同。
你可以通过调用内建函数 cap 很轻松地判断一个通道是否带有缓冲。
如果想异步地执行发送操作,但通道却是非缓冲的,那么就请另行异步化,比如:启用额外的goroutine执行此操作。
在执行接收操作时通常无需关心通道是否带有缓冲,不过有时候也可以依据通道的容量实施不同的接收策略。