这个系列会介绍 golang 常见的坑。当然很多坑是由于对 golang 理解不到位引起的。
这是一段很简单的代码,生产者 go 程打印数字,结束之后发送 cancel 信号。 是不是认为会打印 0-999。如果是这样想的可以继续往下看。
package main
import (
"context"
"fmt"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
data := make(chan int, 1000)
go func() {
for i := 0; i < 1000; i++ {
data <- i
}
cancel()
}()
for {
select {
case <-ctx.Done():
return
case v := <-data:
fmt.Printf("%d\n", v)
}
}
}
你以为会打印 0-999 ?其实不是。。运行下代码你会发现。输出是随机的。what? 这其实和 select 的机制有关系。当 case 条件有多个为真,就想象成随机函数从 case 里面选择一个执行 。上面的代码是两个条件都满足,调用 cancel 函数,有些数据还缓存在 data chan 里面,ctx.Done()条件也为真。选择到 ctx.Done()的时候,这里很可能 case v:=<-data 都没打印全。
刚刚聊了 case 的内部逻辑。再聊下如何解决这个问题。data 每个发送的数据都确保消费掉,最后再调用 cancel 函数就可解决这个问题。做法把带缓冲的 chan 修改为不带缓冲。
// data := make(chan int, 1000)
data := make(chan int)
如果不是必须的理由要用带缓冲的 chan。推荐使用无缓冲的 chan。至于担心的性能问题,他们性能差距不大。后面会补上 benchmark。
1
petelin 2019-10-02 11:24:45 +08:00 via iPhone
这样最后一个 data 不还是有可能打不出来么
|
2
petelin 2019-10-02 11:25:46 +08:00 via iPhone
看错了..
|
3
useben 2019-10-02 11:28:16 +08:00
这是你使用有误。一般不是用 context 来通知 chan 写完的,而是关闭 chan,不然可能会造成泄漏。写端应在写完 close chan,读端应检测 chan 再读 chan,chan 返回 false 表明已被关闭,就退出 for
|
4
guonaihong OP @useben useben 兄,用的方式是 data chan 既要当数据通道,又要当结束控制通道。上面的例子是控制和数据分离的作用。有些场景只能用控制和数据分离的写法,个人觉得没有对错之分。
|
5
guonaihong OP 写错两个字,纠正下。
@useben useben 兄,用的方式是 data chan 既要当数据通道,又要当结束控制通道。上面的例子是控制和数据分离的写法。有些场景只能用控制和数据分离的方法,个人觉得没有对错之分。 |
6
heimeil 2019-10-02 12:27:31 +08:00
|
7
znood 2019-10-02 12:55:05 +08:00
这不能叫 Golang 有坑,只能叫你 Golang 没学好
context 对 select 做中断处理不管你有没有执行完才是正常情况,如果想要处理完就用其他方法比如三楼的方法 再说你对这个处理的问题 // data := make(chan int, 1000) data := make(chan int) 你以为这样就能保证万无一失了吗?你也说了 select 是随机的,但是如果把 fmt.Printf("%d\n", v)换成处理时间长的,这个时候 data <- 999 放进去了,cancel()也执行了,你觉得 select 是一定会选择从 data 读数据吗? |
8
lishunan246 2019-10-02 12:55:38 +08:00
所以为啥这里要用 context 呢
|
9
guonaihong OP @lishunan246 也可以用 done := make(chan struct{}) 这种方式。自从 go1.7 引入 context 之后,现在都用 context 代替 done 的做法。因为很多标准库的参数是 context,后面如果遇到 done 结束还要控制标准库的函数,就不需要修改了。
|
10
guonaihong OP @znood 你没有明白代码。无缓存 chan 是生产者,消费者同步的。data<-999 写进入 并且返回了。代表消费者已经消费调了。这时候调用 cancel 是安全的。
|
11
guonaihong OP @heimeil 兄弟,我假期用的这台电脑不能翻墙。可否贴下代码,学习下。
|
12
Nitroethane 2019-10-02 14:57:00 +08:00 via Android
cancel 不能在这个协程函数中调用吧,因为你不能保证在调用 cancel 之前 select 中的第二个 case 把数据读完啊,虽然无缓冲能解决这个问题,但是在实际业务中肯定要用到有缓冲的 channel 吧
|
13
znood 2019-10-02 15:06:46 +08:00
好吧,献丑了,忘了无缓存 channel 是阻塞的了
不过这里用 cancel 肯定是不合适的,因为你想把队列读取完,又不想关闭 channel,这个时候用 time.After,ctx 无条件返回,读取 channel 超时(队列空)返回 for { select { case <-ctx.Done(): return case <-time.After(time.Second): return case v := <-data: fmt.Printf("%d\n", v) } } |
14
guonaihong OP @znood 这个例子里面不需要 time.After。data chan 消费完。生产者调用 cancel,这时候消费者的 case <- ctx.Done() 就可以返回了。
|
15
heimeil 2019-10-02 16:03:15 +08:00
package main
import ( "context" "fmt" "time" ) func main() { ctx, cancel := context.WithCancel(context.Background()) data := make(chan int, 10) go func() { for i := 0; i < 10; i++ { data <- i } cancel() fmt.Println("cancel") }() for { select { case <-ctx.Done(): fmt.Println("Done") return case v := <-data: doSomething(v) RL: for { select { case v := <-data: doSomething(v) default: break RL } } } } } func doSomething(v int) { time.Sleep(time.Millisecond * 100) fmt.Println(v) } |
16
guonaihong OP @znood 你是想说,如果不用无缓冲 chan。用超时退出?
|
17
reus 2019-10-02 17:08:53 +08:00 2
如果看过 go tour 应该都会知道: https://tour.golang.org/concurrency/5
|
18
such 2019-10-02 17:52:51 +08:00 via iPhone
context 有点滥用了,context 的设计初衷应该是做协程的上下文透传和串联,但是这个例子不涉及到这种场景,都是同一个协程,感觉还是去用另一个 chan 传递退出的信号量
|
19
guonaihong OP @such 和 such 兄想得相反,我倒是不觉得滥用。很多时候一个技术被滥用是带来了性能退化,这里没有性能退化。再者 context 源码里面也是 close chan 再实现通知的。和自己 close chan 来没啥区别。
|
20
guonaihong OP @reus 感谢分享。
|
21
guonaihong OP @heimeil 如果 chan 是带缓冲的,并且因为某些原因不能修改为无缓冲的,可以用下面的该法。你的代码我看了,用两层 for 循环的做法,本质还是想知道 chan 有没有空。直接用个判断就行。
```go ackage main import ( "context" "fmt" "time" ) func main() { ctx, cancel := context.WithCancel(context.Background()) data := make(chan int, 10) go func() { for i := 0; i < 10; i++ { data <- i } cancel() fmt.Println("cancel") }() for { select { case <-ctx.Done(): if len(data) == 0 { fmt.Println("Done") return } case v := <-data: fmt.Printf("v = %d\n", v) } } } ``` |
22
heimeil 2019-10-02 22:40:32 +08:00
并不是判断为空的意思,你可以这样试试看:
case <-ctx.Done(): if len(data) == 0 { fmt.Println("Done") return } else { fmt.Println("--------------") } |
23
znood 2019-10-03 16:45:50 +08:00
肯定要用带缓冲的,不带缓冲的两遍阻塞用两个协程没有意义,用一个协程就处理了
|