91超碰碰碰碰久久久久久综合_超碰av人澡人澡人澡人澡人掠_国产黄大片在线观看画质优化_txt小说免费全本

溫馨提示×

溫馨提示×

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

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

Golang協程泄露怎么預防

發布時間:2022-03-31 11:34:11 來源:億速云 閱讀:303 作者:小新 欄目:編程語言

這篇文章主要介紹了Golang協程泄露怎么預防,具有一定借鑒價值,感興趣的朋友可以參考下,希望大家閱讀完這篇文章之后大有收獲,下面讓小編帶著大家一起了解一下。

泄露案例

關于協程泄露很多時候我們往往會忽略它,直到機器資源負載異常才引起重視。 之前排除生產環境異常的時候,曾經遇到過go程序內存泄露的場景,內存泄漏和協程泄露有很大關系,本質上都是資源不回收導致的。

這里列舉一個典型的泄露案例:

func JumpForSignal() int {
    ch := make(chan int)
    go func() {
        ch <- bizMtx
    }()

    go func() {
        ch <- bizMtx
    }()

    go func() {
        ch <- bizMtx
    }()
    //一有輸入立刻返回
    return <-ch
}

func main() {
    // ...
    JumpForSignal()
    // ...
}

事后分析這個demo可以得知,這個函數調用會阻塞兩個子協程,預期只有一個協程會正常退出。

獲取協程信息

既然存在協程泄露,我們在日常工作怎么避免或者發現它呢?下面我們列舉幾個思路。

遵守準則

由于Go是自帶GC的語言,很多時候寫代碼不需要關心變量的資源釋放,不像C程序員變量申請之后需要在結束處釋放。但是Go的chan在使用時候是有一些準則的,當確定chan不再使用時候,可以在輸出方進行close,避免其他協程還在等待該chan的輸出。

協程數量

找到泄露的協程,第一個能夠想到的是協程數量,當你的函數處理邏輯比較簡單,除了主協程之外,預期協程應該都在結束前返回,可以在main函數結束處調用runtime包的函數:

// NumGoroutine returns the number of goroutines that currently exist.
func NumGoroutine() int {
    return int(gcount())
}

通過它可以返回當前協程總數量:

func Count()  {
    fmt.Printf("Number of goroutines:%d\n", runtime.NumGoroutine())
}

func main() {
    defer Count()
    Count()
    JumpForSignal()
}

輸出:

Number of goroutines:1
Number of goroutines:3

協程函數棧

還有一種比較常見定位協程的形式,在Go里面,可以用于分析協程函數的上下文,常見的比如go自帶的pprof也是通過這種方式獲取,實際案例中,條件允許的情況可以開啟pprof方便分析。

下面來看一個示例,我們在上面的例子加一個http端口監聽,用于接入go自帶的pprof分析工具。

隨后在瀏覽器輸入:

http://localhost:8899/debug/pprof/goroutine?debug=1

可以得到整個程序的協程列表:

goroutine profile: total 7
1 @ 0x165eb6 0x126465 0x126235 0x29341e 0x19de01
#	0x29341d	pixelgo/leak.JumpForSignal.func1+0x3d	F:/code/pixelGo/src/pix-demo/leak/leak.go:24

1 @ 0x165eb6 0x126465 0x126235 0x29347e 0x19de01
#	0x29347d	pixelgo/leak.JumpForSignal.func2+0x3d	F:/code/pixelGo/src/pix-demo/leak/leak.go:28

1 @ 0x165eb6 0x15bb3d 0x1975a5 0x228d05 0x229d8d 0x22c40d 0x321765 0x33437c 0x447c89 0x285239 0x285606 0x4493f3 0x450da8 0x19de01
#	0x1975a4	internal/poll.runtime_pollWait+0x64	D:/dev/go1.16/src/runtime/netpoll.go:227
#	0x228d04	internal/poll.(*pollDesc).wait+0xa4	D:/dev/go1.16/src/internal/poll/fd_poll_runtime.go:87
#	0x229d8c	internal/poll.execIO+0x2ac		D:/dev/go1.16/src/internal/poll/fd_windows.go:175
#	0x22c40c	internal/poll.(*FD).Read+0x56c		

// ...

結論是:當前程序一共有7個協程,可以看出分別有1個協程分配在F:/code/pixelGo/src/pix-demo/leak/leak.go:24F:/code/pixelGo/src/pix-demo/leak/leak.go:28,正是上文泄露的代碼塊。

有時候還可以多維度去分析,比如輸入:

http://localhost:8899/debug/pprof/goroutine?debug=2

可以通過協程后面的標簽,看到當前協程的不同狀態,running/io wait/chan send

goroutine 9 [running]:
runtime/pprof.writeGoroutineStacks(0x7f7d00, 0xc0000aa000, 0x0, 0x0)
	D:/dev/go1.16/src/runtime/pprof/pprof.go:693 +0xc5
net/http/pprof.handler.ServeHTTP(0xc000094011, 0x9, 0x7fba40, 0xc0000aa000, 0xc000092000)
    //..

goroutine 1 [IO wait]:
internal/poll.runtime_pollWait(0x223debb10d8, 0x72, 0xc000152f48)
	D:/dev/go1.16/src/runtime/netpoll.go:227 +0x65
internal/poll.(*pollDesc).wait(0xc0001530b8, 0x72, 0x93b400, 0x0, 0x0)
    //...

goroutine 6 [chan send]:
pixelgo/rout.JumpForSignal.func1(0xc000053800)
	F:/code/pixelGo/src/pix-demo/rout/leak.go:25 +0x10e
created by pixelgo/rout.JumpForSignal
	F:/code/pixelGo/src/pix-demo/rout/leak.go:23 +0x71

goroutine 7 [chan send]:
pixelgo/rout.JumpForSignal.func2(0xc000053800)
	F:/code/pixelGo/src/pix-demo/rout/leak.go:30 +0x10e
created by pixelgo/rout.JumpForSignal
	F:/code/pixelGo/src/pix-demo/rout/leak.go:28 +0x93

協程id

接下來我們來探索協程標識:協程id,在Go中,每個運行的協程都會分配一個協程id,一個常見的方式是從函數運行棧獲取,引用之前網上其他同學的寫法:

func main() {
    fmt.Println(getGID())
}

func getGID() uint64 {
    b := make([]byte, 64)
    b = b[:runtime.Stack(b, false)]
    b = bytes.TrimPrefix(b, []byte("goroutine "))
    b = b[:bytes.IndexByte(b, ' ')]
    n, _ := strconv.ParseUint(string(b), 10, 64)
    return n
}

我們來看看runtime.stack() 會返回什么呢,其中真實內容是這樣的:

goroutine 21 [running]:
leaktest.interestingGoroutines(0xdb9980, 0xc00038e018, 0x0, 0x0, 0x0)
	F:/code/pixelGo/src/leaktest/leaktest.go:81 +0xbf
leaktest.CheckContext(0xdbe398, 0xc000108040, 0xdb9980, 0xc00038e018, 0x0)
	F:/code/pixelGo/src/leaktest/leaktest.go:141 +0x6e
leaktest.CheckTimeout(0xdb9980, 0xc00038e018, 0x3b9aca00, 0x0)
	F:/code/pixelGo/src/leaktest/leaktest.go:127 +0xe5
leaktest.TestCheck.func8(0xc000384780)
	F:/code/pixelGo/src/leaktest/leaktest_test.go:122 +0xaf
testing.tRunner(0xc000384780, 0xc000100050)
	D:/dev/go1.16/src/testing/testing.go:1193 +0x1a3
created by testing.(*T).Run
	D:/dev/go1.16/src/testing/testing.go:1238 +0x63c

goroutine 1 [chan receive]:
testing.(*T).Run(0xc000037080, 0xd8486a, 0x9, 0xd9ebc8, 0x304bd824304bd800)
	D:/dev/go1.16/src/testing/testing.go:1239 +0x66a
testing.runTests.func1(0xc000036f00)
	D:/dev/go1.16/src/testing/testing.go:1511 +0xbd
testing.tRunner(0xc000036f00, 0xc00008fc00)
	D:/dev/go1.16/src/testing/testing.go:1193 +0x1a3
testing.runTests(0xc0000040d8, 0xf40460, 0x5, 0x5, 0x0, 0x0, 0x0, 0x21cbf1c0100)
	D:/dev/go1.16/src/testing/testing.go:1509 +0x448
testing.(*M).Run(0xc0000c0000, 0x0)
	D:/dev/go1.16/src/testing/testing.go:1417 +0x514
main.main()
	_testmain.go:51 +0xc8

可以發現這個棧和我們運行panic拋出的信息非常類似,需要注意的是,通過這種方式獲取協程id并不是一個高效的方式。
實際生產使用過程并不提倡,值得一提的是,為了方便我們更好的定位問題上下文,有時候日志框架又需要我們打印出當前協程id。

比如這是一個生產案例日志輸出:

// gid-1號協程用于初始化資源
[0224/162532.310:INFO:gid-1:yx_trace.go:66] cfg:&{ false false [] 0xc000295140 0xc0001d4e00 <nil> <nil> <nil>}
[0224/162532.320:INFO:gid-1:main.go:50] GameRoom Startup->
[0224/162532.320:INFO:gid-1:config_manager.go:107] configManager SetHttpListenAddr:8080
[0224/162532.320:INFO:gid-1:room_manager.go:57] roomManager Startup
[0224/162532.323:INFO:gid-1:room_manager.go:72] roomManager initPrx.
[0224/162532.330:INFO:gid-1:bootstrap.go:153] GameRoom START ok.
// gid-60號協程分配用于啟動HTTP Server
[0224/162533.277:INFO:gid-60:expose.go:36] Start for HTTP server...
[0224/162533.277:INFO:gid-60:expose.go:39] register for debug server...

往往日志框架是力求對業務性能影響最低的,既然有性能顧慮,那么它是怎么獲取協程id的呢?只能曲線救國了。
還有一個解法,其實在Go中,每個協程綁定的系統線程結構中,有一個g指針,拿到g指針的信息之后,根據g指針結構的偏移量(注意不同go版本可能不同),指定獲取id。

匯編獲取

通過協程綁定的g指針,這里參考《Go高級編程》的做法

// 記錄各個版本的偏移量
var offsetDictMap = map[string]int64{
        "go1.12":    152,
        "go1.12.1":  152,
        "go1.12.2":  152,
        "go1.12.3":  152,
        "go1.12.4":  152,
        "go1.12.5":  152,
        "go1.12.6":  152,
        "go1.12.7":  152,
        "go1.13":    152,
        "go1.14":    152,
        "go1.16.12":    152,
}

// offset for go1.12
var goid_offset uintptr = 152
//go:nosplit
func getG() interface{}

func GoId() int64

// 部分匯編代碼
// func getGptr() unsafe.Pointer
TEXT ·getGptr(SB), NOSPLIT, $0-8
    MOVQ (TLS), BX
    MOVQ BX, ret+0(FP)
    RET

TEXT ·GoId(SB),NOSPLIT,$0-8
    NO_LOCAL_POINTERS
    MOVQ ·goid_offset(SB),AX
    // get runtime.g
    MOVQ (TLS),BX
    ADDQ BX,AX
    MOVQ (AX),BX
    MOVQ BX,ret+0(FP)
    RET

這里點到為止,大概思路是這樣。

性能比較:

我們來簡單測試下兩種獲取go協程id方式性能差距:

// BenchmarkGRtId-8   	1000000000	         0.0005081 ns/op
func BenchmarkGRtId(b *testing.B) {
    for n := 0; n < 1000000000; n++ {
        // runtime獲取協程id
        getGID()
    }
}

// BenchmarkGoId-8   	1000000000	         0.05731 ns/op
func BenchmarkGoId(b *testing.B) {
    for n := 0; n < 1000000000; n++ {
        // 匯編方式獲取
        GoId()
    }
}

可以看到通過匯編方式獲取協程id的方式性能更優,相差幾個數量級。


限制協程

上面列舉了幾個定位協程信息的方法,那么在協程泄露之前有沒有其他方式對程序的go協程進行管控呢,有個做法是使用強大的channel坐下限制。

拋磚引玉

這里先提供一個簡單的思路,即再包裝一層channel進行保護,

// 限制數量
var LIMIT_G_NUM = make(chan struct{}, 100)

// 需要自定義的處理邏輯
type HandleFun func()

func AsyncGoForHandle(fn HandleFun)  {
    // 計數加一
    LIMIT_G_NUM <- struct{}{}
    go func() {
        defer func() {
            if err := recover(); err != nil {
                log.Fatalf("AsyncGoForHandle recover from err: %v", err)
            }
            // 回收計數
            <-LIMIT_G_NUM
        }()

        // 處理邏輯
        fn()
    }()
}

上面的思路比較簡單,相信大家能看懂,每次需要異步創建協程只要調用AsyncGoForHandle()函數即可,不足之處可能是處理邏輯HandleFun()不夠通用,需要自己定義具體實現。

還有一種方式,就是引入協程池的概念,這里的池子和數據庫連接池有點像,即一開始就預創建好,業務層只要負責提交數據,業界已經有不少成熟的封裝。

成熟方案:tunny

之前看到社區有一個封裝得比較完善的協程池tunny,代碼行數不多,我們來試著拆解分析一下代碼,項目地址:https://github.com/Jeffail/tunny

1、定義處理邏輯接口:

type Worker interface {
    // 自定義邏輯實現,開發者只需要關心入參和出參
    Process(interface{}) interface{}
}

2、包裝worker的輸入源workRequest

type workerWrapper struct {
    // 注入內部實現邏輯
    worker        Worker
    interruptChan chan struct{}

    // 請求來源workRequest
    reqChan chan<- workRequest

    // ...
}

3、輸入源結構

type workRequest struct {
    // 輸入
    jobChan chan<- interface{}

    // 處理結果,即worker.Process()的返回值
    retChan <-chan interface{}

    // ...
}

4、編寫實現類:

我們知道Go的接口遵循鴨子模型: 只要它表現得像個鴨子,它就是鴨子

// Worker實現類
type closureWorker struct {
    processor func(interface{}) interface{}
}

func (w *closureWorker) Process(payload interface{}) interface{} {
    return w.processor(payload)
}

5、定義工作池結構

type Pool struct {
    queuedJobs int64

    // 成員函數,用于"鴨子"實體
    ctor    func() Worker
    workers []*workerWrapper
    reqChan chan workRequest

    workerMut sync.Mutex
}

func NewFunc(n int, f func(interface{}) interface{}) *Pool {
    return New(n, func() Worker {
        return &closureWorker{
            // 傳入真正的實現模塊
            processor: f,
        }
    })
}

func New(n int, ctor func() Worker) *Pool {
    p := &Pool{
        ctor:    ctor,
        reqChan: make(chan workRequest),
    }
    // 批量創建協程,監聽處理來自reqChan的任務
    p.SetSize(n)

    return p
}

相關實體結構如下,配合源碼閱讀就比較清晰了。

Golang協程泄露怎么預防

這個框架相當于把協程預先創建好做了池化,隨后業務層只需要源源不斷把"加工數據"輸入到workRequest這個chan即可,也就是process()函數,process()模塊會把數據輸入到內部channel進行處理,池中的worker會進行加工。
這種工廠模式還是值得借鑒的,Go也有很多成熟框架使用了這種寫法。

引用原項目README.md的用法示例:

numCPUs := runtime.NumCPU()

pool := tunny.NewFunc(numCPUs, func(payload interface{}) interface{} {
    var result []byte
    // 關心業務層的輸入、輸出即可
    result = wrapSomething()
    return result
})
defer pool.Close()

http.HandleFunc("/work", func(w http.ResponseWriter, r *http.Request) {
    input, err := ioutil.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "Internal error", http.StatusInternalServerError)
    }
    defer r.Body.Close()

    // 提交任務給Process
    result := pool.Process(input)

    w.Write(result.([]byte))
})

http.ListenAndServe(":8080", nil)

總結

  • Go協程有幾個內置信息,協程id、協程棧、協程狀態(running/io wait/chan send),通過這些信息可以幫助我們一定程度的避免或者定位問題

  • Go里面創建協程只需要一個Go關鍵字,但是要合理回收卻很關鍵,必要時可以用協程池做限制

感謝你能夠認真閱讀完這篇文章,希望小編分享的“Golang協程泄露怎么預防”這篇文章對大家有幫助,同時也希望大家多多支持億速云,關注億速云行業資訊頻道,更多相關知識等著你來學習!

向AI問一下細節
推薦閱讀:
  1. lua 協程
  2. GO協程

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

AI

容城县| 江门市| 天水市| 富顺县| 沙湾县| 利川市| 靖州| 邵东县| 都兰县| 合阳县| 鄱阳县| 青海省| 远安县| 阳春市| 满城县| 黑水县| 康乐县| 青河县| 志丹县| 榕江县| 云安县| 蓬溪县| 嘉鱼县| 霍邱县| 沧州市| 尼玛县| 申扎县| 建德市| 张家口市| 玉门市| 故城县| 梓潼县| 乐亭县| 云安县| 丹巴县| 原平市| 湖口县| 汕尾市| 丰顺县| 武功县| 白朗县|