中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Go中time.After可能導致的內存泄露問題怎么解決

發布時間:2023-05-05 11:03:24 來源:億速云 閱讀:141 作者:iii 欄目:開發技術

本篇內容主要講解“Go中time.After可能導致的內存泄露問題怎么解決”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“Go中time.After可能導致的內存泄露問題怎么解決”吧!

    一、Time 包中定時器函數

    go v1.20.4

    定時函數:NewTicker,NewTimer 和 time.After 介紹

    time 包中有 3 個比較常用的定時函數:NewTicker,NewTimer 和 time.After:

    • NewTimer: 表示在一段時間后才執行,默認情況下執行一次。如果想再次執行,需要調用 time.Reset() 方法,這時類似于 NewTicker 定時器了。可以調用 stop 方法停止執行。

     func NewTimer(d Duration) *Timer
      // NewTimer 創建一個新的 Timer,它將至少持續時間 d 之后,在向通道中發送當前時間
      // d 表示間隔時間
     type Timer struct {
      	C <-chan Time
    	r runtimeTimer
      }

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

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

    如果程序已經從 t.C 接收到了一個值,則已知定時器已過期且通道值已取空,可以直接調用 time.Reset 方法;

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

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

    if !t.Stop() {
    	<-t.C
    }
    t.Reset(d)
    • Stop 方法

    func (t *Timer) Stop() bool
    // 如果定時器已經過期或停止,返回 false,否則返回 true

    Stop 方法能夠阻止定時器觸發,但是它不會關閉通道,這是為了防止從通道中錯誤的讀取值。

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

    if !t.Stop() {
     <-t.C
    }
    • NewTicker: 表示每隔一段時間運行一次,可以執行多次。可以調用 stop 方法停止執行。

    func NewTicker(d Duration) *Ticker

    NewTicker 返回一個 Ticker,這個 Ticker 包含一個時間的通道,每次重置后會發送一個當前時間到這個通道上。

    d 表示每一次運行間隔的時間。

    • time.After: 表示在一段時間后執行。其實它內部調用的就是 time.Timer 。

    func After(d Duration) <-chan Time

    跟它還有一個相似的函數 time.AfterFunc,后面運行的是一個函數。

    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 導致的內存泄露

    基本用法

    time.After 方法是在一段時間后返回 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 實現。

    一般可以用來實現超時檢測:

    package main
    import (
    	"fmt"
    	"time"
    )
    func main() {
    	ch2 := make(chan string, 1)
    	go func() {
    		time.Sleep(time.Second * 2)
    		ch2 <- "hello"
    	}()
    	select {
    	case res := <-ch2:
    		fmt.Println(res)
    	case <-time.After(time.Second * 1):
    		fmt.Println("timeout")
    	}
    }

    有問題代碼

    上面的代碼運行是沒有什么問題的,不會導致內存泄露。

    那問題會出在什么地方?

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

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

    timeafter.go:

    package main
    import (
    	"fmt"
    	"net/http"
    	_ "net/http/pprof"
    	"time"
    )
    func main() {
    	fmt.Println("start...")
    	ch2 := make(chan string, 120)
    	go func() {
    		// time.Sleep(time.Second * 1)
    		i := 0
    		for {
    			i++
    			ch2 <- fmt.Sprintf("%s %d", "hello", i)
    		}
    	}()
    	go func() {
    		// http 監聽8080, 開啟 pprof
    		if err := http.ListenAndServe(":8080", nil); err != nil {
    			fmt.Println("listen failed")
    		}
    	}()
    	for {
    		select {
    		case _ = <-ch2:
    			// fmt.Println(res)
    		case <-time.After(time.Minute * 3):
    			fmt.Println("timeout")
    		}
    	}
    }

    在終端上運行代碼:go run timeafter.go

    然后在開啟另一個終端運行:go tool pprof -http=:8081 http://localhost:8080/debug/pprof/heap ,

    運行之后它會自動在瀏覽器上彈出 pprof 的瀏覽界面,http://localhost:8081/ui/ 。

    本機運行一段時間后比較卡,也說明程序有問題。可以在運行一段時間后關掉運行的 Go 程序,避免電腦卡死。

    用pprof分析問題代碼

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

    Go中time.After可能導致的內存泄露問題怎么解決

    從上圖可以看出,內存使用暴漲(不關掉程序還會繼續漲)。而且暴漲的內存集中在 time.After 上,上面分析了 time.After 實質調用的就是 time.NewTimer,從圖中也可以看出。它調用 time.NewTimer 不斷創建和申請內存,何以看出這個?繼續看下面分析,

    再來看看哪段代碼內存使用最高,還是用 pprof 來查看,瀏覽 http://localhost:8081/ui/source

    timeafter.go

    Go中time.After可能導致的內存泄露問題怎么解決

    上面調用的 Go 源碼 NewTimer,

    Go中time.After可能導致的內存泄露問題怎么解決

    Go中time.After可能導致的內存泄露問題怎么解決

    從上圖數據分析可以看出最占用內存的那部分代碼,src/time/sleep.go/NewTimer 里的 c 和 t 分配和申請內存,最占用內存。

    如果不強行關閉運行程序,這里內存還會往上漲。

    為什么會出現內存一直漲呢?

    在程序中加了 for 循環,for 循環都會不斷調用 select,而每次調用 select,都會重新初始化一個新的定時器 Timer(調用time.After,一直調用它就會一直申請和創建內存),這個新的定時器會增加到時間堆中等待觸發,而定時器啟動前,垃圾回收器不會回收 Timer(Go源碼注釋中有解釋),也就是說 time.After 創建的內存資源需要等到定時器執行完后才被 GC 回收,一直增加內存 GC 卻不回收,內存肯定會一直漲。

    當然,內存一直漲最重要原因還是 for 循環里一直在申請和創建內存,其它是次要 。

    // 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
    }
    // 在經過 d 時段后,會發送值到通道上,并返回通道。
    // 底層就是 NewTimer(d).C。
    // 定時器Timer啟動前不會被垃圾回收器回收,定時器執行后才會被回收。
    // 如果擔心效率問題,可以使用 NewTimer 代替,如果不需要定時器可以調用 Timer.Stop 停止定時器。

    在上面的程序中,time.After(time.Minute * 3) 設置了 3 分鐘,也就是說 3 分鐘后才會執行定時器任務。而這期間會不斷被 for 循環調用 time.After,導致它不斷創建和申請內存,內存就會一直往上漲。

    那怎么解決循環調用的問題?解決了,就可能解決內存一直往上漲的問題。

    解決問題

    既然是 for 循環一直調用 time.After 導致內存暴漲問題,那不循環調用 time.After 行不行?

    修改后的代碼如下:

    package main
    import (
    	"fmt"
    	"net/http"
    	_ "net/http/pprof"
    	"time"
    )
    func main() {
    	fmt.Println("start...")
    	ch2 := make(chan string, 120)
    	go func() {
    		// time.Sleep(time.Second * 1)
    		i := 0
    		for {
    			i++
    			ch2 <- fmt.Sprintf("%s %d", "hello", i)
    		}
    	}()
    	go func() {
    		// http 監聽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 _ = <-ch2:
    			// fmt.Println(res)
    		case <-timeout:
    			fmt.Println("timeout")
    			return
    		}
    	}
    }

    在終端上運行代碼,go run timeafter1.go

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

    自動在瀏覽器上彈出界面 http://localhost:8081/ui/ ,我這里測試,界面沒有任何數據顯示,說明修改后的程序運行良好。

    在 Go 的源碼中 After 函數注釋說了為了更有效率,可以使用 NewTimer ,那我們使用這個函數來改造上面的代碼,

    package main
    import (
    	"fmt"
    	"net/http"
    	_ "net/http/pprof"
    	"time"
    )
    func main() {
    	fmt.Println("start...")
    	ch2 := make(chan string, 120)
    	go func() {
    		// time.Sleep(time.Second * 1)
    		i := 0
    		for {
    			i++
    			ch2 <- fmt.Sprintf("%s %d", "hello", i)
    		}
    	}()
    	go func() {
    		// http 監聽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 _ = <-ch2:
    			// fmt.Println(res)
    		case <-timer.C:
    			fmt.Println("timeout")
    			return
    		}
    	}
    }

    在上面的實現中,也把 NewTimer 放在循環外面,并且每次循環中都調用了 Reset 方法重置定時時間。

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

    三、網上一些錯誤分析

    for循環每次select的時候,都會實例化一個一個新的定時器。該定時器在多少分鐘后,才會被激活,但是激活后已經跟select無引用關系,被gc給清理掉。換句話說,被遺棄的time.After定時任務還是在時間堆里面,定時任務未到期之前,是不會被gc清理的

    上面這種分析說明,最主要的還是沒有說清楚內存暴漲的真正內因。如果用 pprof 的 source 分析查看,就一目了然,那就是 NewTimer 里的 2 個變量創建和申請內存導致的。

    到此,相信大家對“Go中time.After可能導致的內存泄露問題怎么解決”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!

    向AI問一下細節

    免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

    AI

    宣化县| 乌拉特后旗| 梅河口市| 施秉县| 玉门市| 九寨沟县| 吴旗县| 隆安县| 淅川县| 台安县| 蒙自县| 四子王旗| 阳城县| 珠海市| 黎城县| 望城县| 科技| 嫩江县| 柳江县| 高雄县| 淮阳县| 雷波县| 叶城县| 海门市| 德安县| 岳阳县| 九江县| 蓬莱市| 衢州市| 阜阳市| 呼伦贝尔市| 耒阳市| 阳城县| 玛多县| 道孚县| 噶尔县| 民勤县| 西平县| 高邑县| 湟中县| 建始县|