记一个golang channel select的坑
先上代码:
package main
import (
"fmt"
"time"
)
func main() {
stop := make(chan struct{})
i := 0
go channel(i, stop)
wait := make(chan struct{})
wait <- struct{}{}
}
func channel(i int, stop chan struct{}) {
select {
case stop <- struct{}{}:
default:
break
}
for {
select {
case <-stop:
fmt.Println("stop:", i)
return
default:
fmt.Println("channel:", i)
go channel(i+1, stop)
//下面的低吗将导致stop信号几乎无法接收到,进而导致协程无法退出
time.Sleep(time.Second)
}
}
}
这段代码可以在go playground上测试,点击这里
上面的代码本来希望在调用channel()
这个方法的时候,停止掉其他的协程(如果已经有开启的话)。运用场景如:客户端与服务器端通过websocket进行交互,当用户订阅某一类消息时(比如股票行情数据),在新的订阅提交后须取消之前其他的订阅,否则将导致已不需要的订阅数据继续在发送。
但是实际上,上面这段代码的运行结果并不能达到预期效果,输出类似如下:
channel: 0
channel: 1
channel: 2
channel: 3
channel: 4
...
可以发现几乎没有任何协程被停止,越到后面恐怖,严重的情况会把服务器的资源耗尽。
最主要的问题在于default
分支的time.Sleep(time.Second)
,这里的Sleep
将导致发送停止信号的case ch <- struct{}{}:
这里执行变得几乎无法成功。
对代码改造如下:
package main
import (
"fmt"
"time"
)
func main() {
stop := make(chan struct{})
i := 0
go channel(i, stop)
wait := make(chan struct{})
wait <- struct{}{}
}
func channel(i int, stop chan struct{}) {
select {
case stop <- struct{}{}:
default:
break
}
timer := time.NewTimer(time.Millisecond)
ticker := time.NewTicker(time.Second)
for {
select {
case <-stop:
fmt.Println("stop:", i)
return
case <-ticker.C:
fmt.Println("channel:", i)
// 这里用来模拟用户二次订阅
go channel(i+1, stop)
// 通过下面的分支可以立即执行而不用等ticker
case <-timer.C:
timer.Stop()
fmt.Println("channel:", i)
}
}
}
上面的代码能如预期地执行,输入结果类似如下:
channel: 0
channel: 0
stop: 0
channel: 1
channel: 1
...
想在 go playground上执行,点击这里
博主