Go如何防止goroutine泄露

這篇文章主要介紹“Go如何防止goroutine泄露”,在日常操作中,相信很多人在Go如何防止goroutine泄露問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”Go如何防止goroutine泄露”的疑惑有所幫助!接下來,請跟著小編一起來學(xué)習(xí)吧!

成都創(chuàng)新互聯(lián)公司,為您提供重慶網(wǎng)站建設(shè)、網(wǎng)站制作、網(wǎng)站營銷推廣、網(wǎng)站開發(fā)設(shè)計,對服務(wù)成都服務(wù)器租用等多個行業(yè)擁有豐富的網(wǎng)站建設(shè)及推廣經(jīng)驗。成都創(chuàng)新互聯(lián)公司網(wǎng)站建設(shè)公司成立于2013年,提供專業(yè)網(wǎng)站制作報價服務(wù),我們深知市場的競爭激烈,認真對待每位客戶,為客戶提供賞心悅目的作品。 與客戶共同發(fā)展進步,是我們永遠的責(zé)任!

概述

Go 的并發(fā)模型與其他語言不同,雖說它簡化了并發(fā)程序的開發(fā)難度,但如果不了解使用方法,常常會遇到 goroutine 泄露的問題。雖然 goroutine 是輕量級的線程,占用資源很少,但如果一直得不到釋放并且還在不斷創(chuàng)建新協(xié)程,毫無疑問是有問題的,并且是要在程序運行幾天,甚至更長的時間才能發(fā)現(xiàn)的問題。

對于上面描述的問題,我覺得可以從兩方面入手解決,如下:

一是預(yù)防,要做到預(yù)防,我們就需要了解什么樣的代碼會產(chǎn)生泄露,以及了解正確的寫法是如何的;

二是監(jiān)控,雖說預(yù)防減少了泄露產(chǎn)生的概率,但沒有人敢說自己不犯錯,因而,通常我們還需要一些監(jiān)控手段進一步保證程序的健壯性;

接下來,我將會分兩篇文章分別從這兩個角度進行介紹,今天先談第一點。

如何監(jiān)控泄露

本文主要集中在第一點上,但為了更好的演示效果,可以先介紹一個最簡單的監(jiān)控方式。通過 runtime.NumGoroutine() 獲取當(dāng)前運行中的 goroutine 數(shù)量,通過它確認是否發(fā)生泄漏。它的使用非常簡單,就不為它專門寫個例子了。

一個簡單的例子

語言級別的并發(fā)支持是 Go 的一大優(yōu)勢,但這個優(yōu)勢也很容易被濫用。通常我們在開始 Go 并發(fā)學(xué)習(xí)時,常常聽別人說,Go 的并發(fā)非常簡單,在調(diào)用函數(shù)前加上 go 關(guān)鍵詞便可啟動 goroutine,即一個并發(fā)單元,但很多人可能只聽到了這句話,然后就出現(xiàn)了類似下面的代碼:

package main

import (
    "fmt"
    "runtime"
    "time"
)

func sayHello() {
    for {
        fmt.Println("Hello gorotine")
        time.Sleep(time.Second)
    }
}

func main() {
    defer func() {
        fmt.Println("the number of goroutines: ", runtime.NumGoroutine())
    }()

    go sayHello()
    fmt.Println("Hello main")
}

對 Go 比較熟悉的話,很容易發(fā)現(xiàn)這段代碼的問題,sayHello 是個死循環(huán),沒有如何退出機制,因此也就沒有任何辦法釋放創(chuàng)建的 goroutine。我們通過在 main 函數(shù)最前面的 defer 實現(xiàn)在函數(shù)退出時打印當(dāng)前運行中的 goroutine 數(shù)量,毫無意外,它的輸出如下:

the number of goroutines: 2

不過,因為上面的程序并非常駐,有泄露問題也不大,程序退出后系統(tǒng)會自動回收運行時資源。但如果這段代碼在常駐服務(wù)中執(zhí)行,比如 http server,每接收到一個請求,便會啟動一次 sayHello,時間流逝,每次啟動的 goroutine 都得不到釋放,你的服務(wù)將會離奔潰越來越近。

這個例子比較簡單,我相信,對 Go 的并發(fā)稍微有點了解的朋友都不會犯這個錯。

泄露情況分類

前面介紹的例子由于在 goroutine 運行死循環(huán)導(dǎo)致的泄露。接下來,我會按照并發(fā)的數(shù)據(jù)同步方式對泄露的各種情況進行分析。簡單可歸于兩類,即:

  • channel 導(dǎo)致的泄露

  • 傳統(tǒng)同步機制導(dǎo)致的泄露

傳統(tǒng)同步機制主要指面向共享內(nèi)存的同步機制,比如排它鎖、共享鎖等。這兩種情況導(dǎo)致的泄露還是比較常見的。go 由于 defer 的存在,第二類情況,一般情況下還是比較容易避免的。

chanel 引起的泄露

先說 channel,如果之前讀過官方的那篇并發(fā)的文章,翻譯版,你會發(fā)現(xiàn) channel 的使用,一個不小心就泄露了。我們來具體總結(jié)下那些情況下可能導(dǎo)致。

發(fā)送不接收

我們知道,發(fā)送者一般都會配有相應(yīng)的接收者。理想情況下,我們希望接收者總能接收完所有發(fā)送的數(shù)據(jù),這樣就不會有任何問題。但現(xiàn)實是,一旦接收者發(fā)生異常退出,停止繼續(xù)接收上游數(shù)據(jù),發(fā)送者就會被阻塞。這個情況在 前面說的文章 中有非常細致的介紹。

示例代碼:

package main

import "time"

func gen(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        for _, n := range nums {
            out <- n
        }
        close(out)
    }()
    return out
}

func main() {
    defer func() {
        fmt.Println("the number of goroutines: ", runtime.NumGoroutine())
    }()

    // Set up the pipeline.
    out := gen(2, 3)

    for n := range out {
        fmt.Println(n)               // 2
        time.Sleep(5 * time.Second) // done thing, 可能異常中斷接收
        if true { // if err != nil 
            break
        }
    }
}

例子中,發(fā)送者通過 out chan 向下游發(fā)送數(shù)據(jù),main 函數(shù)接收數(shù)據(jù),接收者通常會依據(jù)接收到的數(shù)據(jù)做一些具體的處理,這里用 Sleep 代替。如果這期間發(fā)生異常,導(dǎo)致處理中斷,退出循環(huán)。gen 函數(shù)中啟動的 goroutine 并不會退出。

如何解決?

此處的主要問題在于,當(dāng)接收者停止工作,發(fā)送者并不知道,還在傻傻地向下游發(fā)送數(shù)據(jù)。故而,我們需要一種機制去通知發(fā)送者。我直接說答案吧,就不循漸進了。Go 可以通過 channel 的關(guān)閉向所有的接收者發(fā)送廣播信息。

修改后的代碼:

package main

import "time"

func gen(done chan struct{}, nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for _, n := range nums {
            select {
            case out <- n:
            case <-done:
                return
            }
        }
    }()
    return out
}

func main() {
    defer func() {
        time.Sleep(time.Second)
        fmt.Println("the number of goroutines: ", runtime.NumGoroutine())
    }()

    // Set up the pipeline.
    done := make(chan struct{})
    defer close(done)

    out := gen(done, 2, 3)

    for n := range out {
        fmt.Println(n) // 2
        time.Sleep(5 * time.Second) // done thing, 可能異常中斷接收
        if true { // if err != nil 
            break
        }
    }
}

函數(shù) gen 中通過 select 實現(xiàn) 2 個 channel 的同時處理。當(dāng)異常發(fā)生時,將進入 <-done 分支,實現(xiàn) goroutine 退出。這里為了演示效果,保證資源順利釋放,退出時等待了幾秒保證釋放完成。

執(zhí)行后的輸出如下:

the number of goroutines:  1

現(xiàn)在只有主 goroutine 存在。

接收不發(fā)送

發(fā)送不接收會導(dǎo)致發(fā)送者阻塞,反之,接收不發(fā)送也會導(dǎo)致接收者阻塞。直接看示例代碼,如下:

package main

func main() {
    defer func() {
        time.Sleep(time.Second)
        fmt.Println("the number of goroutines: ", runtime.NumGoroutine())
    }()

    var ch chan struct{}
    go func() {
        ch <- struct{}{}
    }()
}

運行結(jié)果顯示:

the number of goroutines:  2

當(dāng)然,我們正常不會遇到這么傻的情況發(fā)生,現(xiàn)實工作中的案例更多可能是發(fā)送已完成,但是發(fā)送者并沒有關(guān)閉 channel,接收者自然也無法知道發(fā)送完畢,阻塞因此就發(fā)生了。

解決方案是什么?那當(dāng)然就是,發(fā)送完成后一定要記得關(guān)閉 channel。

nil channel

向 nil channel 發(fā)送和接收數(shù)據(jù)都將會導(dǎo)致阻塞。這種情況可能在我們定義 channel 時忘記初始化的時候發(fā)生。

示例代碼:

func main() {
    defer func() {
        time.Sleep(time.Second)
        fmt.Println("the number of goroutines: ", runtime.NumGoroutine())
    }()

    var ch chan int
    go func() {
        <-ch
        // ch<-
    }()
}

兩種寫法:<-ch 和 ch<- 1,分別表示接收與發(fā)送,都將會導(dǎo)致阻塞。如果想實現(xiàn)阻塞,通過 nil channel 和 done channel 結(jié)合實現(xiàn)阻止 main 函數(shù)的退出,這或許是可以一試的方法。

func main() {
	defer func() {
		time.Sleep(time.Second)
		fmt.Println("the number of goroutines: ", runtime.NumGoroutine())
	}()

	done := make(chan struct{})

	var ch chan int
	go func() {
		defer close(done)
	}()

	select {
	case <-ch:
	case <-done:
		return
	}
}

在 goroutine 執(zhí)行完成,檢測到 done 關(guān)閉,main 函數(shù)退出。

真實的場景

真實的場景肯定不會像案例中的簡單,可能涉及多階段 goroutine 之間的協(xié)作,某個 goroutine 可能即使接收者又是發(fā)送者。但歸根接底,無論什么使用模式。都是把基礎(chǔ)知識組織在一起的合理運用。

傳統(tǒng)同步機制

雖然,一般推薦 Go 并發(fā)數(shù)據(jù)的傳遞,但有些場景下,顯然還是使用傳統(tǒng)同步機制更合適。Go 中提供傳統(tǒng)同步機制主要在 sync 和 atomic 兩個包。接下來,我主要介紹的是鎖和 WaitGroup 可能導(dǎo)致 goroutine 的泄露。

Mutex

和其他語言類似,Go 中存在兩種鎖,排它鎖和共享鎖,關(guān)于它們的使用就不作介紹了。我們以排它鎖為例進行分析。

示例如下:

func main() {
    total := 0

    defer func() {
        time.Sleep(time.Second)
        fmt.Println("total: ", total)
        fmt.Println("the number of goroutines: ", runtime.NumGoroutine())
    }()

    var mutex sync.Mutex
    for i := 0; i < 2; i++ {
        go func() {
            mutex.Lock()
            total += 1
        }()
    }
}

執(zhí)行結(jié)果如下:

total: 1
the number of goroutines: 2

這段代碼通過啟動兩個 goroutine 對 total 進行加法操作,為防止出現(xiàn)數(shù)據(jù)競爭,對計算部分做了加鎖保護,但并沒有及時的解鎖,導(dǎo)致 i = 1 的 goroutine 一直阻塞等待 i = 0 的 goroutine 釋放鎖??梢钥吹?,退出時有 2 個 goroutine 存在,出現(xiàn)了泄露,total 的值為 1。

怎么解決?因為 Go 有 defer 的存在,這個問題還是非常容易解決的,只要記得在 Lock 的時候,記住 defer Unlock 即可。

示例如下:

mutex.Lock()
defer mutext.Unlock()

其他的鎖與這里其實都是類似的。

WaitGroup

WaitGroup 和鎖有所差別,它類似 Linux 中的信號量,可以實現(xiàn)一組 goroutine 操作的等待。使用的時候,如果設(shè)置了錯誤的任務(wù)數(shù),也可能會導(dǎo)致阻塞,導(dǎo)致泄露發(fā)生。

一個例子,我們在開發(fā)一個后端接口時需要訪問多個數(shù)據(jù)表,由于數(shù)據(jù)間沒有依賴關(guān)系,我們可以并發(fā)訪問,示例如下:

package main

import (
    "fmt"
    "runtime"
    "sync"
    "time"
)

func handle() {
    var wg sync.WaitGroup

    wg.Add(4)

    go func() {
        fmt.Println("訪問表1")
        wg.Done()
    }()

    go func() {
        fmt.Println("訪問表2")
        wg.Done()
    }()

    go func() {
        fmt.Println("訪問表3")
        wg.Done()
    }()

    wg.Wait()
}

func main() {
    defer func() {
        time.Sleep(time.Second)
        fmt.Println("the number of goroutines: ", runtime.NumGoroutine())
    }()

    go handle()
    time.Sleep(time.Second)
}

執(zhí)行結(jié)果如下:

the number of goroutines: 2

出現(xiàn)了泄露。再看代碼,它的開始部分定義了類型為 sync.WaitGroup 的變量 wg,設(shè)置并發(fā)任務(wù)數(shù)為 4,但是從例子中可以看出只有 3 個并發(fā)任務(wù)。故最后的 wg.Wait() 等待退出條件將永遠無法滿足,handle 將會一直阻塞。

怎么防止這類情況發(fā)生?

我個人的建議是,盡量不要一次設(shè)置全部任務(wù)數(shù),即使數(shù)量非常明確的情況。因為在開始多個并發(fā)任務(wù)之間或許也可能出現(xiàn)被阻斷的情況發(fā)生。最好是盡量在任務(wù)啟動時通過 wg.Add(1) 的方式增加。

示例如下:

    ...
    wg.Add(1)
    go func() {
        fmt.Println("訪問表1")
        wg.Done()
    }()

    wg.Add(1)
    go func() {
        fmt.Println("訪問表2")
        wg.Done()
    }()

    wg.Add(1)
    go func() {
        fmt.Println("訪問表3")
        wg.Done()
    }()
    ...

總結(jié)

大概介紹完了我認為的所有可能導(dǎo)致 goroutine 泄露的情況。總結(jié)下來,其實無論是死循環(huán)、channel 阻塞、鎖等待,只要是會造成阻塞的寫法都可能產(chǎn)生泄露。因而,如何防止 goroutine 泄露就變成了如何防止發(fā)生阻塞。為進一步防止泄露,有些實現(xiàn)中會加入超時處理,主動釋放處理時間太長的 goroutine。

本篇主要從如何寫出正確代碼的角度來介紹如何防止 goroutine 的泄露。下篇[https://juejin.im/post/5d3d76066fb9a07ee463aba0],將會介紹如何實現(xiàn)更好的監(jiān)控檢測,以幫助我們發(fā)現(xiàn)當(dāng)前代碼中已經(jīng)存在的泄露。

到此,關(guān)于“Go如何防止goroutine泄露”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識,請繼續(xù)關(guān)注創(chuàng)新互聯(lián)網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬嵱玫奈恼拢?/p>

當(dāng)前標(biāo)題:Go如何防止goroutine泄露
當(dāng)前路徑:http://muchs.cn/article6/jpeiog.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站內(nèi)鏈品牌網(wǎng)站設(shè)計、用戶體驗、自適應(yīng)網(wǎng)站、網(wǎng)站營銷

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)

網(wǎng)站優(yōu)化排名