〇
- 无缓冲的channel读和写都是阻塞的
- 理解读是阻塞的比较容易,读不到就等着
- 理解写是阻塞的则稍微有点抽象,简单理解就是无缓冲channel没有缓冲区,左右两端必须同时准备好才能进行一次传输,否则就等着,读等写,或者写等读
- 对于有缓冲区但缓冲区已满的channel来说,读和写的表现基本类似,除了单次读取和写入的不是同一个值而已
// test0.go
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int)
go func() {
fmt.Println("to in")
c <- 1
fmt.Println("in")
}()
fmt.Println("one second")
time.Sleep(time.Second)
fmt.Println("to out")
fmt.Println(<-c)
fmt.Println("out")
}
/* go run test0.go
one second
to in
to out
1
out
*/
- 打印
to out
之前等待了一秒 - 这个例子就是被阻塞的写操作,在没有读操作之前,一直阻塞在写操作,没有执行到打印
in
- 由于主程序打印
out
之后便结束,goroutine里的in
不一定会打印出来
我们来再看一个例子
// test00.go
package main
import (
"fmt"
"time"
)
func main() {
tt := time.NewTicker(time.Second)
fmt.Println(time.Now())
time.Sleep(time.Second * 5)
fmt.Println(time.Now())
fmt.Println(<-tt.C)
}
/* go run test00.go
2009-11-10 23:00:00 +0000 UTC m=+0.000000001
2009-11-10 23:00:05 +0000 UTC m=+5.000000001
2009-11-10 23:00:01 +0000 UTC m=+1.000000001
*/
- 这个例子同样是被阻塞的写操作
- ticker在00秒被设置
- ticker在01秒向信号channel中发送当前的时间
- sleep5秒
- 打印05秒时间
- 读操作,获取并打印01秒ticker就一直在等待发送的channel数据
- 这个读写操作之间的时间差就是延迟
一
- channel被close之前读取数据是阻塞式的,如果读不到数据就会阻塞
- channel被close之后可以非阻塞的获得数据类型的零值
- 接收channel的数据可以使用双返回值
v, ok := <-c
,ok为true表示v来自于channel的正常输入(不管当前channel状态),false表示channel已close,获取的v是数据类型的零值
// test1.go
package main
import (
"fmt"
)
func main() {
c := make(chan int, 1)
c <- 1
v, ok := <-c
fmt.Println(v, ok)
c <- 2
close(c)
v, ok = <-c
fmt.Println(v, ok)
v, ok = <-c
fmt.Println(v, ok)
deadlock()
}
func deadlock() {
c := make(chan int, 1)
//close(c)
v, ok := <-c
fmt.Println(v, ok)
}
/* go run test1.go
1 true
2 true
0 false
fatal error: all goroutines are asleep - deadlock!
...
*/
注:
c <- 2
后close(c)
,从channel中获取2,虽然channel已close,但ok仍为true,表示2是来自于channel的正常输入- 而之后再获取数据,ok就是false了,所以ok并不是表示当前channel的状态,而是跟v相关,表示获取的数据v是否是正常传入的
二
- 使用range读取channel需要配合close操作,否则会死锁
// test2.go
package main
import (
"fmt"
)
func main() {
c := make(chan int, 3)
c <- 1
c <- 2
c <- 3
close(c)
for v := range c {
fmt.Println(v)
}
deadlock()
}
func deadlock() {
c := make(chan int, 3)
c <- 1
c <- 2
c <- 3
for v := range c {
fmt.Println(v)
}
}
/* go run test2.go
1
2
3
1
2
3
fatal error: all goroutines are asleep - deadlock!
...
*/
三
- 常用time.NewTicker获取一个计时器(time.Ticker)和其脉冲信号channel(time.Ticker.C)
- time.Ticker的Stop()方法并不close这个time.Ticker的信号channel
- 并且这个channel是只读的,调用方也不能直接close,因而不要指望range这个channel可以正常退出,如果要用range来读取这个信号channel则需要自己做退出控制
// test3.go
package main
import (
"fmt"
"time"
)
func main() {
cc := time.NewTicker(100 * time.Millisecond)
ff := false
go func() {
time.Sleep(time.Second)
ff = true
//time.Sleep(time.Second)
//cc.Stop()
}()
for v := range cc.C {
if ff {
break
}
fmt.Println(v)
}
}
func deadlock() {
cc := time.NewTicker(100 * time.Millisecond)
go func() {
time.Sleep(1 * time.Second)
cc.Stop()
}()
for v := range cc.C {
fmt.Println(v)
}
}
/* go run test3.go
2023-08-09 12:31:40.331341 +0800 CST m=+0.101223710
2023-08-09 12:31:40.431214 +0800 CST m=+0.201093376
2023-08-09 12:31:40.531207 +0800 CST m=+0.301083918
2023-08-09 12:31:40.631233 +0800 CST m=+0.401107210
2023-08-09 12:31:40.731235 +0800 CST m=+0.501105501
2023-08-09 12:31:40.831061 +0800 CST m=+0.600928376
2023-08-09 12:31:40.931265 +0800 CST m=+0.701129376
2023-08-09 12:31:41.031248 +0800 CST m=+0.801109085
2023-08-09 12:31:41.130913 +0800 CST m=+0.900771210
2023-08-09 12:31:41.231256 +0800 CST m=+1.001111751
*/
注:
- 先解开main中的
cc.Stop()
的注释,运行,会死锁,因为不再往信号channel发送信号了(并不是close),range cc.C
阻塞了(不一定,我在本机上运行上面的程序会死锁,在https://go.dev/play/中运行则不会死锁) - 注意,上面这个方法并不一定会导致死锁,在不同的环境中运行会导致不同的结果。为什么呢?实际上是在goroutine里
time.Sleep(time.Second)
之后的ff = true
和cc.Stop()
之间,对信号channel的读取打了一个时间差 - 再解开
time.Sleep(time.Second)
的注释,运行,可以正常退出了,因为信号channel被额外维持生效,继续向信号channel中发出信号,下一个信号获取后,才得以运行到检查ff为true并执行break - 这种退出控制是延迟控制,并不保险,使用的时候要尽量搞清楚其执行顺序
- 注意,上例中可能还存在goroutine竞争使用ff的问题
总之,尽量避免上面这种有歧义的使用方法。
四
- 分享一个排空channel的函数,用到了范型(注意close掉channel)
- 来自于:https://taoshu.in/go/generics/examples.html
- 文中还有其他有趣的示例(以范型为主)
func Drain[T any](c <-chan T) {
for range c {
}
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。