golang 使用channel实现素数筛并深入理解goroutine泄漏

select 基础回顾:

可以看到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 阻塞在genPrimeNumberch <- 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泄漏, 阻塞在 primeNumberFilterout <- 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 无法退出而导致泄漏

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值