国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

Go坑:time.After可能導(dǎo)致的內(nèi)存泄露問題分析

這篇具有很好參考價(jià)值的文章主要介紹了Go坑:time.After可能導(dǎo)致的內(nèi)存泄露問題分析。希望對(duì)大家有所幫助。如果存在錯(cuò)誤或未考慮完全的地方,請(qǐng)大家不吝賜教,您也可以點(diǎn)擊"舉報(bào)違法"按鈕提交疑問。

Go 中 time.After 可能導(dǎo)致的內(nèi)存泄露

一、Time 包中定時(shí)器函數(shù)

go v1.20.4

定時(shí)函數(shù):NewTicker,NewTimer 和 time.After 介紹

time 包中有 3 個(gè)比較常用的定時(shí)函數(shù):NewTicker,NewTimer 和 time.After:

  • NewTimer: 表示在一段時(shí)間后才執(zhí)行,默認(rèn)情況下執(zhí)行一次。如果想再次執(zhí)行,需要調(diào)用 time.Reset() 方法,這時(shí)類似于 NewTicker 定時(shí)器了??梢哉{(diào)用 stop 方法停止執(zhí)行。
  func NewTimer(d Duration) *Timer
  // NewTimer 創(chuàng)建一個(gè)新的 Timer,它將至少持續(xù)時(shí)間 d 之后,在向通道中發(fā)送當(dāng)前時(shí)間
  // d 表示間隔時(shí)間
  
 type Timer struct {
  	C <-chan Time
	r runtimeTimer
  }

重置 NewTimer 定時(shí)器的 Reset() 方法,它是定時(shí)器在持續(xù)時(shí)間 d 到期后,用這個(gè)方法重置定時(shí)器讓它再一次運(yùn)行,如果定時(shí)器被激活返回 true,如果定時(shí)器已過(guò)期或停止,在返回 false。

func (t *Timer) Reset(d Duration) bool
  • 用 Reset 方法需要注意的地方:

如果程序已經(jīng)從 t.C 接收到了一個(gè)值,則已知定時(shí)器已過(guò)期且通道值已取空,可以直接調(diào)用 time.Reset 方法;

如果程序尚未從 t.C 接收到值,則要先停止定時(shí)器 t.Stop(),再?gòu)?t.C 中取出值,最后調(diào)用 time.Reset 方法。

綜合上面 2 種情況,正確使用 time.Reset 方法就是:

if !t.Stop() {
	<-t.C
}
t.Reset(d)
  • Stop 方法
func (t *Timer) Stop() bool
// 如果定時(shí)器已經(jīng)過(guò)期或停止,返回 false,否則返回 true

Stop 方法能夠阻止定時(shí)器觸發(fā),但是它不會(huì)關(guān)閉通道,這是為了防止從通道中錯(cuò)誤的讀取值。

為了確保調(diào)用 Stop 方法后通道為空,需要檢查 Stop 方法的返回值并把通道中的值清空,如下:

if !t.Stop() {
 <-t.C
}
  • NewTicker: 表示每隔一段時(shí)間運(yùn)行一次,可以執(zhí)行多次??梢哉{(diào)用 stop 方法停止執(zhí)行。

    func NewTicker(d Duration) *Ticker
    

    NewTicker 返回一個(gè) Ticker,這個(gè) Ticker 包含一個(gè)時(shí)間的通道,每次重置后會(huì)發(fā)送一個(gè)當(dāng)前時(shí)間到這個(gè)通道上。

    d 表示每一次運(yùn)行間隔的時(shí)間。

  • time.After: 表示在一段時(shí)間后執(zhí)行。其實(shí)它內(nèi)部調(diào)用的就是 time.Timer 。

    func After(d Duration) <-chan Time
    

? 跟它還有一個(gè)相似的函數(shù) time.AfterFunc,后面運(yùn)行的是一個(gè)函數(shù)。

NewTicker 代碼例子:

package main

import (
	"fmt"
	"time"
)

func main() {
	ticker := time.NewTicker(time.Second)
	defer ticker.Stop()
	done := make(chan bool)
	go func() {
		time.Sleep(10 * time.Second)
		done <- true
	}()
	for {
		select {
		case <-done:
			fmt.Println("Done!")
			return
		case t := <-ticker.C:
			fmt.Println("Current time: ", t)
		}
	}
}

二、time.After 導(dǎo)致的內(nèi)存泄露

基本用法

time.After 方法是在一段時(shí)間后返回 time.Time 類型的 channel 消息,看下面源碼就清楚返回值類型:

// https://github.com/golang/go/blob/go1.20.4/src/time/sleep.go#LL156C1-L158C2
func After(d Duration) <-chan Time {
	return NewTimer(d).C
}

// https://github.com/golang/go/blob/go1.20.4/src/time/sleep.go#LL50C1-L53C2
type Timer struct {
	C <-chan Time
	r runtimeTimer
}

從代碼可以看出它底層就是 NewTimer 實(shí)現(xiàn)。

一般可以用來(lái)實(shí)現(xiàn)超時(shí)檢測(cè):

package main

import (
	"fmt"
	"time"
)

func main() {
	ch1 := make(chan string, 1)

	go func() {
		time.Sleep(time.Second * 2)
		ch1 <- "hello"
	}()

	select {
	case res := <-ch1:
		fmt.Println(res)
	case <-time.After(time.Second * 1):
		fmt.Println("timeout")
	}
}

有問題代碼

上面的代碼運(yùn)行是沒有什么問題的,不會(huì)導(dǎo)致內(nèi)存泄露。

那問題會(huì)出在什么地方?

在有些情況下,select 需要配合 for 不斷檢測(cè)通道情況,問題就有可能出在 for 循環(huán)這里。

修改上面的代碼,加上 for + select,為了能顯示的看出問題,加上 pprof + http 代碼,

timeafter.go:

package main

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

func main() {
	fmt.Println("start...")
	ch1 := make(chan string, 120)

	go func() {
		// time.Sleep(time.Second * 1)
		i := 0
		for {
			i++
			ch1 <- fmt.Sprintf("%s %d", "hello", i)
		}

	}()

	go func() {
		// http 監(jiān)聽8080, 開啟 pprof
		if err := http.ListenAndServe(":8080", nil); err != nil {
			fmt.Println("listen failed")
		}
	}()

	for {
		select {
		case _ = <-ch1:
			// fmt.Println(res)
		case <-time.After(time.Minute * 3):
			fmt.Println("timeout")
		}
	}
}

在終端上運(yùn)行代碼:go run timeafter.go,

然后在開啟另一個(gè)終端運(yùn)行:go tool pprof -http=:8081 http://localhost:8080/debug/pprof/heap

運(yùn)行之后它會(huì)自動(dòng)在瀏覽器上彈出 pprof 的瀏覽界面,http://localhost:8081/ui/ 。

本機(jī)運(yùn)行一段時(shí)間后比較卡,也說(shuō)明程序有問題??梢栽谶\(yùn)行一段時(shí)間后關(guān)掉運(yùn)行的 Go 程序,避免電腦卡死。

用pprof分析問題代碼

在瀏覽器上查看 pprof 圖,http://localhost:8081/ui/ ,

Go坑:time.After可能導(dǎo)致的內(nèi)存泄露問題分析

從上圖可以看出,內(nèi)存使用暴漲(不關(guān)掉程序還會(huì)繼續(xù)漲)。而且暴漲的內(nèi)存集中在 time.After 上,上面分析了 time.After 實(shí)質(zhì)調(diào)用的就是 time.NewTimer,從圖中也可以看出。它調(diào)用 time.NewTimer 不斷創(chuàng)建和申請(qǐng)內(nèi)存,何以看出這個(gè)?繼續(xù)看下面分析,

再來(lái)看看哪段代碼內(nèi)存使用最高,還是用 pprof 來(lái)查看,瀏覽 http://localhost:8081/ui/source

timeafter.go

Go坑:time.After可能導(dǎo)致的內(nèi)存泄露問題分析

上面調(diào)用的 Go 源碼 NewTimer,

Go坑:time.After可能導(dǎo)致的內(nèi)存泄露問題分析

Go坑:time.After可能導(dǎo)致的內(nèi)存泄露問題分析

從上圖數(shù)據(jù)分析可以看出最占用內(nèi)存的那部分代碼,src/time/sleep.go/NewTimer 里的 c 和 t 分配和申請(qǐng)內(nèi)存,最占用內(nèi)存。

如果不強(qiáng)行關(guān)閉運(yùn)行程序,這里內(nèi)存還會(huì)往上漲。

為什么會(huì)出現(xiàn)內(nèi)存一直漲呢?

在程序中加了 for 循環(huán),for 循環(huán)都會(huì)不斷調(diào)用 select,而每次調(diào)用 select,都會(huì)重新初始化一個(gè)新的定時(shí)器 Timer(調(diào)用time.After,一直調(diào)用它就會(huì)一直申請(qǐng)和創(chuàng)建內(nèi)存),這個(gè)新的定時(shí)器會(huì)增加到時(shí)間堆中等待觸發(fā),而定時(shí)器啟動(dòng)前,垃圾回收器不會(huì)回收 Timer(Go源碼注釋中有解釋),也就是說(shuō) time.After 創(chuàng)建的內(nèi)存資源需要等到定時(shí)器執(zhí)行完后才被 GC 回收,一直增加內(nèi)存 GC 卻不回收,內(nèi)存肯定會(huì)一直漲。

當(dāng)然,內(nèi)存一直漲最重要原因還是 for 循環(huán)里一直在申請(qǐng)和創(chuàng)建內(nèi)存,其它是次要 。

// https://github.com/golang/go/blob/go1.20.4/src/time/sleep.go#LL150C1-L158C2

// 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.
func After(d Duration) <-chan Time {
	return NewTimer(d).C
}
// 在經(jīng)過(guò) d 時(shí)段后,會(huì)發(fā)送值到通道上,并返回通道。
// 底層就是 NewTimer(d).C。
// 定時(shí)器Timer啟動(dòng)前不會(huì)被垃圾回收器回收,定時(shí)器執(zhí)行后才會(huì)被回收。
// 如果擔(dān)心效率問題,可以使用 NewTimer 代替,如果不需要定時(shí)器可以調(diào)用 Timer.Stop 停止定時(shí)器。

在上面的程序中,time.After(time.Minute * 3) 設(shè)置了 3 分鐘,也就是說(shuō) 3 分鐘后才會(huì)執(zhí)行定時(shí)器任務(wù)。而這期間會(huì)不斷被 for 循環(huán)調(diào)用 time.After,導(dǎo)致它不斷創(chuàng)建和申請(qǐng)內(nèi)存,內(nèi)存就會(huì)一直往上漲。

那怎么解決循環(huán)調(diào)用的問題?解決了,就可能解決內(nèi)存一直往上漲的問題。

解決問題

既然是 for 循環(huán)一直調(diào)用 time.After 導(dǎo)致內(nèi)存暴漲問題,那不循環(huán)調(diào)用 time.After 行不行?

修改后的代碼如下:

package main

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

func main() {
	fmt.Println("start...")
	ch1 := make(chan string, 120)

	go func() {
		// time.Sleep(time.Second * 1)
		i := 0
		for {
			i++
			ch1 <- fmt.Sprintf("%s %d", "hello", i)
		}

	}()

	go func() {
		// http 監(jiān)聽8080, 開啟 pprof
		if err := http.ListenAndServe(":8080", nil); err != nil {
			fmt.Println("listen failed")
		}
	}()
	// time.After 放到 for 外面
	timeout := time.After(time.Minute * 3)
	for {
		select {
		case _ = <-ch1:
			// fmt.Println(res)
		case <-timeout:
			fmt.Println("timeout")
			return
		}
	}
}

在終端上運(yùn)行代碼,go run timeafter1.go

等待半分鐘左右,在另外一個(gè)終端上運(yùn)行 go tool pprof -http=:8081 http://localhost:8080/debug/pprof/heap

自動(dòng)在瀏覽器上彈出界面 http://localhost:8081/ui/ ,我這里測(cè)試,界面沒有任何數(shù)據(jù)顯示,說(shuō)明修改后的程序運(yùn)行良好。

在 Go 的源碼中 After 函數(shù)注釋說(shuō)了為了更有效率,可以使用 NewTimer ,那我們使用這個(gè)函數(shù)來(lái)改造上面的代碼,

package main

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

func main() {
	fmt.Println("start...")
	ch1 := make(chan string, 120)

	go func() {
		// time.Sleep(time.Second * 1)
		i := 0
		for {
			i++
			ch1 <- fmt.Sprintf("%s %d", "hello", i)
		}

	}()

	go func() {
		// http 監(jiān)聽8080, 開啟 pprof
		if err := http.ListenAndServe(":8080", nil); err != nil {
			fmt.Println("listen failed")
		}
	}()

	duration := time.Minute * 2
	timer := time.NewTimer(duration)
	defer timer.Stop()
	for {
		timer.Reset(duration) // 這里加上 Reset()
		select {
		case _ = <-ch1:
			// fmt.Println(res)
		case <-timer.C:
			fmt.Println("timeout")
			return
		}
	}
}

在上面的實(shí)現(xiàn)中,也把 NewTimer 放在循環(huán)外面,并且每次循環(huán)中都調(diào)用了 Reset 方法重置定時(shí)時(shí)間。

測(cè)試,運(yùn)行 go run timeafter1.go,然后多次運(yùn)行 go tool pprof -http=:8081 http://localhost:8080/debug/pprof/heap ,查看 pprof,我這里測(cè)試每次數(shù)據(jù)都是空白,說(shuō)明程序正常運(yùn)行。

三、網(wǎng)上一些錯(cuò)誤分析

for循環(huán)每次select的時(shí)候,都會(huì)實(shí)例化一個(gè)一個(gè)新的定時(shí)器。該定時(shí)器在多少分鐘后,才會(huì)被激活,但是激活后已經(jīng)跟select無(wú)引用關(guān)系,被gc給清理掉。換句話說(shuō),被遺棄的time.After定時(shí)任務(wù)還是在時(shí)間堆里面,定時(shí)任務(wù)未到期之前,是不會(huì)被gc清理的

上面這種分析說(shuō)明,最主要的還是沒有說(shuō)清楚內(nèi)存暴漲的真正內(nèi)因。如果用 pprof 的 source 分析查看,就一目了然,那就是 NewTimer 里的 2 個(gè)變量創(chuàng)建和申請(qǐng)內(nèi)存導(dǎo)致的。


也歡迎到我的公眾號(hào) 【九卷技術(shù)錄】 Go坑:time.After可能導(dǎo)致的內(nèi)存泄露問題分析 討論文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-432841.html

四、參考

  • https://pkg.go.dev/time#pkg-overview
  • https://github.com/golang/go/blob/go1.20.4/src/time/sleep.go
  • https://www.cnblogs.com/jiujuan/p/14588185.html pprof 基本使用
  • 《100 Go Mistakes and How to Avoid Them》 作者:Teiva Harsanyi

到了這里,關(guān)于Go坑:time.After可能導(dǎo)致的內(nèi)存泄露問題分析的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來(lái)自互聯(lián)網(wǎng)用戶投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場(chǎng)。本站僅提供信息存儲(chǔ)空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請(qǐng)注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請(qǐng)點(diǎn)擊違法舉報(bào)進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

  • go 內(nèi)存泄露

    go 內(nèi)存泄露

    事件回顧 9.15號(hào)晚18點(diǎn)服務(wù)端發(fā)版 9.16號(hào)晚21點(diǎn)監(jiān)控顯示自發(fā)版后服務(wù)器 TCP_alloc 指標(biāo)一路飆升至40K(如圖) 問題分析 看到 tcp_alloc 指標(biāo)異常,初步懷疑有tcp連接創(chuàng)建后未關(guān)閉,應(yīng)該是上次發(fā)版寫了什么代碼導(dǎo)致的。回顧此次發(fā)版清單,問題應(yīng)該出現(xiàn)在了daemon服務(wù)心跳上報(bào)上。 d

    2024年02月07日
    瀏覽(24)
  • 注意避坑!Java 內(nèi)部類持有外部類會(huì)導(dǎo)致內(nèi)存泄露。。。

    注意避坑!Java 內(nèi)部類持有外部類會(huì)導(dǎo)致內(nèi)存泄露。。。

    本文介紹 Java 內(nèi)部類持有外部類導(dǎo)致內(nèi)存泄露的原因以及其解決方案。 為什么內(nèi)部類持有外部類會(huì)導(dǎo)致內(nèi)存泄露 非靜態(tài)內(nèi)部類會(huì)持有外部類,如果有地方引用了這個(gè)非靜態(tài)內(nèi)部類,會(huì)導(dǎo)致外部類也被引用,垃圾回收時(shí)無(wú)法回收這個(gè)外部類(即使外部類已經(jīng)沒有其他地方在使

    2024年02月09日
    瀏覽(16)
  • .NET 6 在 Win7 系統(tǒng)證書鏈錯(cuò)誤導(dǎo)致 HttpWebRequest 內(nèi)存泄露

    本文記錄我將應(yīng)用遷移到 dotnet 6 之后,在 Win7 系統(tǒng)上,因?yàn)槭褂?HttpWebRequest 訪問一個(gè)本地服務(wù),此本地服務(wù)開啟 https 且證書鏈在此 Win7 系統(tǒng)上錯(cuò)誤,導(dǎo)致應(yīng)用內(nèi)存泄露問題。本文記錄此問題的原因以及調(diào)查過(guò)程 核心原因是在 CRYPT32.dll 上的 CertGetCertificateChain 方法存在內(nèi)存泄

    2024年02月06日
    瀏覽(20)
  • lettuce.core.RedisCommandTimeoutException: Command timed out after 1 minute(s) timeout 60000超時(shí)問題

    lettuce.core.RedisCommandTimeoutException: Command timed out after 1 minute(s) timeout 60000超時(shí)問題

    有一臺(tái)服務(wù)器 java程序不定期會(huì)出現(xiàn)Redis command timed out; nested exception is io.lettuce.core.RedisCommandTimeoutException: Command timed out after 1 minute(s) 錯(cuò)誤,導(dǎo)致應(yīng)用出現(xiàn) timeout 60000 錯(cuò)誤,重啟應(yīng)用后,問題修復(fù),但還是會(huì)不定期出現(xiàn)該問題。查看應(yīng)用日志,發(fā)現(xiàn)有如下錯(cuò)誤: 出現(xiàn)timeout 600

    2024年02月16日
    瀏覽(19)
  • Java 內(nèi)存泄露問題詳解

    目錄 1、什么是內(nèi)存泄露? 2、Java 中可能導(dǎo)致內(nèi)存泄露的場(chǎng)景 3、長(zhǎng)生命周期對(duì)象持有短生命周期對(duì)象引用造成的內(nèi)存泄露問題示例 4、靜態(tài)集合類持有對(duì)象引用造成內(nèi)存泄露問題的示例 1、什么是內(nèi)存泄露? ????????內(nèi)存泄露指的是程序運(yùn)行時(shí)未能正確釋放不再使用的內(nèi)

    2024年02月09日
    瀏覽(25)
  • 記錄線上排查內(nèi)存泄露問題

    記錄線上排查內(nèi)存泄露問題

    記錄一次云上排查內(nèi)存泄露的問題,最近監(jiān)控告警云上有空指針異常報(bào)出,于是找到運(yùn)維查日志定位到具體是哪一行代碼拋出的空指針異常, 發(fā)現(xiàn)是在解析cookie的一個(gè)方法內(nèi),調(diào)用HttpServletRequest.getServerName()獲取不到拋出的NPE,這個(gè)獲取服務(wù)名獲取不到,平時(shí)都沒有出現(xiàn)過(guò)的

    2024年02月03日
    瀏覽(19)
  • 用jprofiler來(lái)分析 jvm 堆 內(nèi)存泄露,fullgc

    用jprofiler來(lái)分析 jvm 堆 內(nèi)存泄露,fullgc

    jvm 命令和工具_(dá)個(gè)人渣記錄僅為自己搜索用的博客-CSDN博客 ? 方法1: 重新設(shè)置堆后,重啟,復(fù)現(xiàn). ? 方法2:? 切割 ?官網(wǎng)文檔 JProfiler Help - HPROF snapshots heap walker教學(xué): 用jprofile查看hprof文件_hprof jprofile_java老張的博客-CSDN博客 通識(shí)教學(xué): JVM監(jiān)控及診斷工具GUI篇之JProfiler_每天都要進(jìn)步一

    2024年02月08日
    瀏覽(24)
  • Java應(yīng)用堆外內(nèi)存泄露問題排查

    最近有個(gè)java應(yīng)用在做壓力測(cè)試 壓測(cè)環(huán)境配置: CentOS系統(tǒng) 4核CPU 8g內(nèi)存 jdk1.6.0_25,jvm配置-server -Xms2048m -Xmx2048m 出現(xiàn)問題如下 執(zhí)行300并發(fā),壓測(cè)持續(xù)1個(gè)小時(shí)后內(nèi)存使用率從20%上升到100%,tps從1100多降低到600多。 首先使用top命令查看內(nèi)存占用如下 然后查看java堆內(nèi)存分布情況,查

    2024年02月12日
    瀏覽(25)
  • 可能導(dǎo)致balenaEtcher寫入出錯(cuò)的原因以及如何解決這些問題

    balenaEtcher是一款流行的免費(fèi)開源燒錄軟件,它能夠幫助用戶將ISO映像文件或者img文件燒錄到USB閃存驅(qū)動(dòng)器或SD卡上。盡管balenaEtcher使用簡(jiǎn)單,但有時(shí)候在燒錄過(guò)程中可能會(huì)出現(xiàn)錯(cuò)誤。下面是一些可能導(dǎo)致balenaEtcher寫入出錯(cuò)的原因以及如何解決這些問題: 1.不正確的映像文件:

    2024年02月05日
    瀏覽(18)
  • JavaScript 使用 splice 方法刪除數(shù)組元素可能導(dǎo)致的問題

    JavaScript 使用 splice 方法刪除數(shù)組元素可能導(dǎo)致的問題

    splice() 方法通過(guò)刪除或替換現(xiàn)有元素或者原地添加新的元素來(lái)修改數(shù)組,并以數(shù)組形式返回被修改的內(nèi)容。此方法會(huì)改變?cè)瓟?shù)組。 JavaScript 遍歷數(shù)組并通過(guò) splice 方法刪除該數(shù)組符合某些條件的元素將會(huì)導(dǎo)致哪些問題? 當(dāng)使用 splice 方法從 JavaScript 數(shù)組中刪除元素時(shí),可能會(huì)

    2023年04月23日
    瀏覽(25)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請(qǐng)作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包