可以看到select的语法结构有点类似于switch,但又有些不同。
select里的case后面并不带判断条件,而是一个信道的操作,不同于switch里的case,对于从其它语言转过来的开发者来说有些需要特别注意的地方。
golang 的 select 就是监听 IO 操作,当 IO 操作发生时,触发相应的动作每个case语句里必须是一个IO操作,确切的说,应该是一个面向channel的IO操作。
注:Go 语言的 select 语句借鉴自 Unix 的 select() 函数,在 Unix 中,可以通过调用 select() 函数来监控一系列的文件句柄,一旦其中一个文件句柄发生了 IO 动作,该 select() 调用就会被返回(C 语言中就是这么做的),后来该机制也被用于实现高并发的 Socket 服务器程序。Go 语言直接在语言级别支持 select关键字,用于处理并发编程中通道之间异步 IO 通信问题。
注意:如果 ch1 或者 ch2 信道都阻塞的话,就会立即进入 default 分支,并不会阻塞。但是如果没有 default 语句,则会阻塞直到某个信道操作成功为止。
知识点
- select语句只能用于信道的读写操作
- select中的case条件(非阻塞)是并发执行的,select会选择先操作成功的那个case条件去执行,如果多个同时返回,则随机选择一个执行,此时将无法保证执行顺序。对于阻塞的case语句会直到其中有信道可以操作,如果有多个信道可操作,会随机选择其中一个 case 执行
- 对于case条件语句中,如果存在信道值为
nil
的读写操作,则该分支将被忽略(编译期?),可以理解为从select语句中删除了这个case语句 - 如果有超时条件语句,判断逻辑为如果在这个时间段内一直没有满足条件的case,则执行这个超时case。如果此段时间内出现了可操作的case,则直接执行这个case。一般用超时语句代替了default语句
- 对于空的select{},可以用作死循环
- 对于for中的select{}, 也有可能会引起cpu占用过高的问题, 需要考虑是否有必要添加default分支.
package main
import (
"fmt"
"runtime"
)
func genPrimeNumber() (ch chan int) {
ch = make(chan int, 1)
go func() {
for i := 2; ; i++ {
ch <- i
// fmt.Printf("[gen %v ] ", i)
}
}()
return
}
func primeNumberFilter(ch <-chan int, p int) (out chan int) {
// fmt.Printf("创建筛子:[%v], [%v]\n", ch, p)
out = make(chan int, 1)
go func() {
for {
n := <-ch
// fmt.Printf("[ch=%v, n=%v , p=%v ]", ch, n, p)
if (n % p) != 0 {
out <- n
}
}
}()
return
}
func main() {
runtime.GOMAXPROCS(1)
ch := genPrimeNumber()
fmt.Printf("&ch = %v\n\n\n", &ch)
for i := 0; i < 4; i++ {
n := <-ch
fmt.Printf(" ====> %v ", n)
ch = primeNumberFilter(ch, n) // 此处返回的是新的channel, 给ch赋值不影响原来的goroutine往原来的channel中发数据
fmt.Printf("new channel = %v\n", ch)
}
fmt.Println()
}
ch = primeNumberFilter(ch, n)
此处返回的是新的channel, 给ch赋值不影响原来的goroutine往原来的channel中发数据
因为chan
类型本身就是指针, 给指针赋值其实修改了指针变量的指向(因为之前的goroutine保持着对channel的引用, 所以channel资源不会被回收) 而不影响之前的goroutine往channel中发数据
摘自原文: 我们先是调用GenerateNatural()生成最原始的从2开始的自然数序列。然后开始一个100次迭代的循环,希望生成100个素数。在每次循环迭代开始的时候,管道中的第一个数必定是素数,我们先读取并打印这个素数。然后基于管道中剩余的数列,并以当前取出的素数为筛子过滤后面的素数。不同的素数筛子对应的管道是串联在一起的。
“串连在一起” 是理解此例子的关键!
参考原文: https://chai2010.cn/advanced-go-programming-book/ch1-basic/ch1-06-goroutine.html
以上面的代码我们使用 pprof
分析资源泄漏的情况
import _ "net/http/pprof"
...
func main() {
...
ip := "0.0.0.0:9001"
if err := http.ListenAndServe(ip, nil); err != nil {
fmt.Printf("start pprof failed on %s\n", ip)
}
}
浏览器打开: http://127.0.0.1:9001/debug/pprof/ , 可以看到有105个goroutine, 其中 100个goroutine是阻塞在 primeNumberFilter
中的out <- n
, 还有一个 goroutine 阻塞在genPrimeNumber
的 ch <- i
, 其他几个是 main和pprof的服务的.
也就是 101
个goroutine泄露了 , 当然, 这个只是示例程序, 并非用于生产环境的代码, 但是, 以此示例展开对"golang中如何防止goroutine泄漏"的思考.
请思考: 如何防止goroutine泄漏?
,
,
,
,
,
,
,
,
,
,
,
,
,
,
使用 context 进行管理:
package main
import (
"context"
"fmt"
"net/http"
_ "net/http/pprof"
"runtime"
"time"
)
func genPrimeNumber(ctx context.Context) (out chan int) {
out = make(chan int)
go func() {
defer fmt.Printf("genPrimeNumber goroutine finised\n")
for i := 2; ; i++ {
select {
case out <- i:
case <-ctx.Done(): // finish this goroutine
return
}
}
}()
return
}
func primeNumberFilter(ctx context.Context, in <-chan int, prime int) (out chan int) {
out = make(chan int)
go func() {
defer fmt.Printf("primeNumberFilter goroutine finised\n")
for {
select {
case n := <-in:
if (n % prime) != 0 {
out <- n
}
case <-ctx.Done():
return // finish this goroutine
}
}
}()
return
}
func main() {
defer func() {
time.Sleep(5 * time.Second)
fmt.Println("the number of goroutines: ", runtime.NumGoroutine())
}()
fmt.Println("the number of goroutines: ", runtime.NumGoroutine()) // 1
ctx, cancel := context.WithCancel(context.Background())
ch := genPrimeNumber(ctx)
fmt.Println("the number of goroutines: ", runtime.NumGoroutine()) // 2
for i := 0; i < 100; i++ {
fmt.Println("the number of goroutines: ", runtime.NumGoroutine())
n := <-ch
ch = primeNumberFilter(ctx, ch, n)
fmt.Println(n)
}
cancel()
time.Sleep(5 * time.Second)
fmt.Println("the number of goroutines: ", runtime.NumGoroutine())
time.Sleep(5 * time.Second)
ip := "0.0.0.0:9001"
if err := http.ListenAndServe(ip, nil); err != nil {
fmt.Printf("start pprof failed on %s\n", ip)
}
}
查看pprof, 有16个goroutine泄漏, 阻塞在 primeNumberFilter
的 out <- n
goroutine profile: total 20
16 @ 0x43b7c5 0x40745c 0x407215 0x770813 0x470061
# 0x770812 main.primeNumberFilter.func1+0x192 /data/QBlockChainNotes/Go语言/Go高级/prime_filter_v2.go:36
请思考: 应该如何解决上述仍然存在的gorutine泄漏的问题?
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
func primeNumberFilter(ctx context.Context, in <-chan int, prime int) (out chan int) {
out = make(chan int)
go func() {
for {
select {
case n := <-in:
if (n % prime) != 0 {
select {
case out <- n:
break
case <-ctx.Done():
return
}
}
case <-ctx.Done():
return
// 取消default会大大提高效率!
// default:
// continue
}
}
}()
return
}
练习: 请思考以下代码是否存在有问题?
func primeNumberFilter(ctx context.Context, in <-chan int, prime int) (out chan int) {
out = make(chan int)
go func() {
for {
if n := <-in; (n % prime) != 0 {
select {
case out <- n:
break
case <-ctx.Done():
return
}
}
}
}()
return
}
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
if n := <-in; (n % prime) != 0
会导致很多goroutine阻塞在 n := <-in 无法退出而导致泄漏