time.Afterとselectを併用したときに存在する「ピット」

昨夜、西風が緑の木々を枯らした

Golangでのselect/#timeout制御の4つの主要な使用法では、time.Afterタイムアウト制御を実現するためにselectが一致していると述べられています。実際、これは問題です。

この書き込み方法は新しい時刻を初期化するためです。毎回、1分などの待ち時間が長くなると、メモリリークが発生します(もちろん、これに限らず、読み続けてください)。

誰が持ってきたのかわかりません。selectとtimeの使用。Goでのタイムアウト制御のAfterは、ほぼ事実上の標準になってい
ます。Golangtime.After()の使用法やコード例などの例は、インターネット上にたくさんあります。

多くの大企業のコードリポジトリに<- time.Afterは、検索用のキーワードがたくさんあり、次の時間の多くは数分です。

pprofで見つけるのは難しくありません。これは教科書レベルのエラーです...毎回初期化されますが、実行前にリサイクルされないため、メモリが急増します。

コードにそのような使用法がたくさんあるかどうかを確認する時間があるときに検索することをお勧めします...

参照することができます

Fengyun-golang時間によって引き起こされるメモリ爆発のOOM問題を分析します。

時間を使用します。注意して後はメモリリークが発生します(golang)

これらの分析、実際の検証

after.go:

package main

import (
	"fmt"
	"net/http"
	_ "net/http/pprof"
	"time"
)

/**
  time.After oom 验证demo
*/
func main() {

	go func() {
		// 开启pprof,监听请求
		if err := http.ListenAndServe(":6060", nil); err != nil { // 也可以写成 127.0.0.1:6060
			fmt.Printf("start pprof failed on %s,err%v \n", "6060", err)
		}
	}()

	ch := make(chan string, 100)
	go func() {
		for {
			ch <- "向管道塞入数据"
		}
	}()

	for {
		select {
		case <-ch:
		case <-time.After(time.Minute * 3):
		}
	}
}

このプログラムを実行してから実行

go tool pprof -http=:8081 http://localhost:6060/debug/pprof/heap

このコマンドラインは、次の3つの部分に分けることができます。

  • -http=:8081これはWebの形式で指定され、ローカルポート8081で開始します
    (パラメーターが追加されていない場合は-http=:8081、コマンドラインインタラクションに入ります。コマンドラインに入力することは、パラメーターを直接web使用することと同じです)-http=:8081

  • http://localhost:6060/debug/pprof/heapプロファイルファイルを取得するために指定するアドレスです。リアルタイムで実行されているローカルプログラムでこの方法を使用できます。多くの場合(サーバー上に、外部に公開されているpprofのポートがない場合など)、最初にマシンにアクセスし、curl http://127.0.0.1:6060/debug/pprof/heap -o heap_cui.outそれを使用してプロファイルファイルを取得できます。次にgo tool pprof --http :9091 heap_cui.out、分析を使用して、ローカルで取得する方法を見つけます。

そして、時間の経過とともに、プログラムによって占有されるメモリは増加し続けます

从调用图可发现, 程序不断调用time.After,进而导致计时器 time.NerTimer 不断创建和内存申请

// After waits for the duration to elapse and then sends the current time
// on the returned channel.
// It is equivalent to NewTimer(d).C.
// The underlying Timer is not recovered by the garbage collector
// until the timer fires. If efficiency is a concern, use NewTimer
// instead and call Timer.Stop if the timer is no longer needed.

//After 等待持续时间过去,然后在返回的通道上发送当前时间。
//它相当于 NewTimer(d).C。
//在定时器触发之前,垃圾收集器不会恢复底层定时器。 如果效率是一个问题,请改用 NewTimer 并在不再需要计时器时调用 Timer.Stop。

func After(d Duration) <-chan Time {
	return NewTimer(d).C
}

在select里面虽然没有执行到time.After,但每次都会初始化,会在时间堆里面,定时任务未到期之前,是不会被gc清理的

  • 在计时器触发之前,垃圾收集器不会回收Timer

  • 如果考虑效率,需要使用NewTimer替代

衣带渐宽终不悔

使用NewTimer 或NewTicker替代:

package main

import (
	"fmt"
	"net/http"
	_ "net/http/pprof"
	"time"
)

/**
  time.After oom 验证demo
*/
func main() {

	go func() {
		// 开启pprof,监听请求
		if err := http.ListenAndServe(":6060", nil); err != nil { // 也可以写成 127.0.0.1:6060
			fmt.Printf("start pprof failed on %s,err%v \n", "6060", err)
		}
	}()

	ticker := time.NewTicker(time.Minute * 3)
    // 或
    //timer := time.NewTimer(3 * time.Minute)
    //defer timer.Stop()
    // 下方的case <-ticker.C:相应改为case <-timer.C:


	ch := make(chan string, 100)
	go func() {
		for {
			ch <- "向管道塞入数据"
		}
	}()

	for {
		select {
		case <-ch:
		case <-ticker.C:
			print("结束执行")
		}
	}

}

这篇Go 内存泄露之痛,这篇把 Go timer.After 问题根因讲透了!应该有点问题,不是内存孤儿,gc还是会去回收的,只是要在time.After到期之后

众里寻他千百度

如上是网上大多数技术文章的情况:

  • 昨夜西风凋碧树,独上高楼,望断天涯路: select + time.After实现超时控制

  • 衣带渐宽终不悔,为伊消得人憔悴: 这样写有问题,会内存泄露,要用NewTimer 或NewTicker替代time.After

其实针对本例,这些说法都没有切中肯綮

最初的代码仅仅是有内存泄露的问题吗?

实际上,即便3分钟后,第2个case也得不到执行 (可以把3min改成2s验证下)

只要第一个case能不断从channel中取出数据(在此显然可以),那第二个case就永远得不到执行。这是因为每次time.After都被重新初始化了,而上面那个case一直满足条件,当然就是第二个case一直得不到执行, 除非第一个case超过3min没有从channel中拿到数据

所以其实在此例中NewTimer还是NewTicker,都不是问题本质,这个问题本质,就是个变量作用域的问题

在for循环外定义time.After(time.Minute * 3),如下:

package main

import (
	"fmt"
	"net/http"
	_ "net/http/pprof"
	"time"
)

func main() {

	go func() {
		// 开启pprof,监听请求
		if err := http.ListenAndServe(":6060", nil); err != nil { // 也可以写成 127.0.0.1:6060
			fmt.Printf("start pprof failed on %s,err%v \n", "6060", err)
		}
	}()

	ch := make(chan string, 100)
	go func() {
		for {
			ch <- "向管道塞入数据"
		}
	}()

	timeout := time.After(time.Minute * 3)
	for {
		select {
		case <-ch:
		case <-timeout:
			fmt.Println("到了这里")
		}
	}
}

時間を入れてください。ループの外に出た後、メモリリークがないことがわかります。3分後(おそらくもう少し)、2番目のケースはスケジュールどおりに実行されます。

したがって、このシナリオでは、時間ではありません。

ガベージコレクターは、タイマーが起動するまでタイマーをリサイクルしません

問題ですが、少なくとも最も無視されている変数スコープの問題です。

(プログラマーのポットは時間の問題ではありません。後... NewTimerまたはNewTickerがメモリをリークしない理由は、forループの外側で初期化されているからです...)

forループでの前回のcase <-time.After(time.Minute * 3)記述では、効果は次のようになります。

package main

import "time"

func main() {

	for {
		time.After(2 * time.Second)
	}

}

いわゆる「メモリオーファン」になるかどうかを確認します

プログラムを変更して、次のことを確認します。

ガベージコレクターは、タイマーが起動するまでタイマーをリサイクルしません

;

しかしで

タイマーが起動した後、ガベージコレクターはこれらのタイマーをリサイクルします

、「メモリの孤立」を引き起こしません

package main

import (
	"fmt"
	"net/http"
	_ "net/http/pprof"
	"sync/atomic"
	"time"
)

func main() {

	go func() {
		// 开启pprof,监听请求
		if err := http.ListenAndServe(":6060", nil); err != nil { // 也可以写成 127.0.0.1:6060
			fmt.Printf("start pprof failed on %s,err%v \n", "6060", err)
		}
	}()

	after()

	fmt.Println("程序结束")

}

func after() {
	var i int32
	ch := make(chan string, 0)
	done := make(chan string) // 设定的时间已到,通知结束循环,不要再往channel里面写数据

	go func() {

		for {

			select {
			default:
				atomic.AddInt32(&i, 1)
				ch <- fmt.Sprintf("%s%d%s", "向管道第", i, "次塞入数据")
			case exit := <-done:
				fmt.Println("关闭通道", exit)
				return
			}

		}
	}()

	go func() {
		time.Sleep(time.Second)
		done <- "去给我通知不要再往ch这个channel里写数据了!"
	}()

	for {
		select {
		case res := <-ch:
			fmt.Println("res:", res)
		case <-time.After(2 * time.Second):
			fmt.Println("结束接收通道的数据")
			return
		}
	}

}

印刷された情報を削除し、現在のリアルタイムメモリ情報に置き換えます。

package main

import (
	"fmt"
	"net/http"
	_ "net/http/pprof"
	"runtime"
	"sync/atomic"
	"time"
)

func main() {

	go func() {
		// 开启pprof,监听请求
		if err := http.ListenAndServe(":6060", nil); err != nil { // 也可以写成 127.0.0.1:6060
			fmt.Printf("start pprof failed on %s,err%v \n", "6060", err)
		}
	}()

	after()

	fmt.Println("程序结束")

}

func after() {
	var ms runtime.MemStats
	runtime.ReadMemStats(&ms)
	fmt.Println("before, have", runtime.NumGoroutine(), "goroutines,", ms.Alloc, "bytes allocated", ms.HeapObjects, "heap object")

	var i int32
	ch := make(chan string, 0)
	done := make(chan string) // 设定的时间已到,通知结束循环,不要再往channel里面写数据

	go func() {

		for {

			select {
			default:
				atomic.AddInt32(&i, 1)
				ch <- fmt.Sprintf("%s%d%s", "向管道第", i, "次塞入数据")
			case exit := <-done:
				fmt.Println("关闭通道", exit)
				return
			}

		}
	}()

	go func() {
		time.Sleep(time.Second)
		done <- "去给我通知不要再往ch这个channel里写数据了!"
	}()

	for {
		select {
		case res := <-ch:
			runtime.GC()
			runtime.ReadMemStats(&ms)
			fmt.Printf("%s,now have %d goroutines,%d bytes allocated, %d heap object \n", res, runtime.NumGoroutine(), ms.Alloc, ms.HeapObjects)
		case <-time.After(2 * time.Second):
			runtime.GC()
			fmt.Println("当前结束接收通道的数据,准备返程")
			runtime.ReadMemStats(&ms)
			fmt.Printf("now have %d goroutines,%d bytes allocated, %d heap object \n", runtime.NumGoroutine(), ms.Alloc, ms.HeapObjects)
			return
		}
	}

}

その他の参考資料:

time.NewTicker()とタイマーに移動します

tech-talk-time。常に初期化された後

おすすめ

転載: juejin.im/post/7118265074543755277