欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Go?中?time.After?可能導致的內(nèi)存泄露問題解析

 更新時間:2023年05月05日 08:24:19   投稿:mrr  
這篇文章主要介紹了Go?中?time.After?可能導致的內(nèi)存泄露,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下

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

go v1.20.4

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

? 跟它還有一個相似的函數(shù) time.AfterFunc,后面運行的是一個函數(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 導致的內(nèi)存泄露

基本用法

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 實現(xiàn)。

一般可以用來實現(xiàn)超時檢測:

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")
	}
}

有問題代碼

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

那問題會出在什么地方?

在有些情況下,select 需要配合 for 不斷檢測通道情況,問題就有可能出在 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")
		}
	}
}

在終端上運行代碼: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/ ,

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

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

timeafter.go

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

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

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

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

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

當然,內(nèi)存一直漲最重要原因還是 for 循環(huán)里一直在申請和創(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)過 d 時段后,會發(fā)送值到通道上,并返回通道。
// 底層就是 NewTimer(d).C。
// 定時器Timer啟動前不會被垃圾回收器回收,定時器執(zhí)行后才會被回收。
// 如果擔心效率問題,可以使用 NewTimer 代替,如果不需要定時器可以調(diào)用 Timer.Stop 停止定時器。

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

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

解決問題

既然是 for 循環(huán)一直調(diào)用 time.After 導致內(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
		}
	}
}

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

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

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

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

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
		}
	}
}

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

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

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

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

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

四、參考

到此這篇關于Go 中 time.After 可能導致的內(nèi)存泄露的文章就介紹到這了,更多相關go time.After 內(nèi)存泄露內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • GoLang的sync.WaitGroup與sync.Once簡單使用講解

    GoLang的sync.WaitGroup與sync.Once簡單使用講解

    sync.WaitGroup類型,它比通道更加適合實現(xiàn)這種一對多的goroutine協(xié)作流程。WaitGroup是開箱即用的,也是并發(fā)安全的。同時,與之前提到的同步工具一樣,它一旦被真正的使用就不能被復制了
    2023-01-01
  • Golang中json和jsoniter的區(qū)別使用示例

    Golang中json和jsoniter的區(qū)別使用示例

    這篇文章主要介紹了Golang中json和jsoniter的區(qū)別使用示例,本文給大家分享兩種區(qū)別,結(jié)合示例代碼給大家介紹的非常詳細,感興趣的朋友跟隨小編一起看看吧
    2023-12-12
  • Golang標準庫container/list的用法圖文詳解

    Golang標準庫container/list的用法圖文詳解

    提到單向鏈表,大家應該是比較熟悉的了,這篇文章主要為大家詳細介紹了Golang標準庫container/list的用法相關知識,感興趣的小伙伴可以了解下
    2024-01-01
  • Go語言里切片slice的用法介紹

    Go語言里切片slice的用法介紹

    這篇文章介紹了Go語言里切片slice的用法,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2022-07-07
  • Go 語言單例模式示例詳解

    Go 語言單例模式示例詳解

    這篇文章主要為大家介紹了Go 語言單例模式示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-10-10
  • go語言題解LeetCode674最長連續(xù)遞增序列

    go語言題解LeetCode674最長連續(xù)遞增序列

    這篇文章主要為大家介紹了go語言題解LeetCode674最長連續(xù)遞增序列示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-12-12
  • Go語言實現(xiàn)基于websocket瀏覽器通知功能

    Go語言實現(xiàn)基于websocket瀏覽器通知功能

    這篇文章主要介紹了Go語言實現(xiàn)基于websocket瀏覽器通知功能,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2020-07-07
  • Golang接口使用教程詳解

    Golang接口使用教程詳解

    在?Go?語言中接口包含兩種含義:它既是方法的集合,?同時還是一種類型并且在Go?語言中是隱式實現(xiàn)的。本文通過示例詳細介紹了Golang接口的使用,需要的可以參考一下
    2022-09-09
  • Go語言并發(fā)之Sync包的6個關鍵概念總結(jié)

    Go語言并發(fā)之Sync包的6個關鍵概念總結(jié)

    這篇文章主要為大家詳細介紹了Go語言并發(fā)中Sync包的6個關鍵概念,文中的示例代碼講解詳細,對我們深入學習Go語言有一定的幫助,需要的可以參考一下
    2023-05-05
  • GO中優(yōu)雅編碼與降低圈復雜度詳析

    GO中優(yōu)雅編碼與降低圈復雜度詳析

    Go語法簡單易用,有其他編程經(jīng)驗的開發(fā)者,相信學習并快速上手Go語言的開發(fā),多數(shù)覺得不困難吧,下面這篇文章主要給大家介紹了關于GO中優(yōu)雅編碼與降低圈復雜度的相關資料,需要的朋友可以參考下
    2022-12-12

最新評論