亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

Golang中errgroup的常見誤用詳解

 更新時間:2024年01月29日 11:16:44   作者:apocelipes  
errgroup和sync.WaitGroup類似,都可以發(fā)起執(zhí)行并等待一組協(xié)程直到所有協(xié)程運行結束,本文主要為大家整理了一些errgroup的常見誤用,有需要的可以參考下

errgroup想必稍有經驗的golang程序員都應該聽說過,實際項目中用過的也應該不在少數(shù)。它和sync.WaitGroup類似,都可以發(fā)起執(zhí)行并等待一組協(xié)程直到所有協(xié)程運行結束。除此之外errgroup還可以在協(xié)程出錯時取消當前的context,以及它還能控制可運行的協(xié)程的數(shù)量。

但在日常的代碼review時我注意到了幾個比較常見的問題,這些問題有的無傷大雅最多只會造成一些性能損失,有的則會導致資源泄露甚至是死鎖崩潰。

這里對這些比較典型的誤用做下記錄。

多余的context嵌套

先說個不是很常見但我還是遇到過兩三次的不太妥當?shù)挠梅ā?/p>

我們知道errgroup在協(xié)程返回錯誤的時候會取消掉創(chuàng)建時傳入的context,這是為了能讓同組的其他協(xié)程知道有錯誤發(fā)生應該盡快退出執(zhí)行。

所以errgroup使用的context應該是派生于當前上下文的新的context,這樣才不會讓可能的取消操作影響到errgroup之外的范圍。

因此第一個常見誤用出現(xiàn)了:

func DoWork(ctx context.Context) {
    errCtx, cancel := context.WithCancel(ctx)
    defer cancel()
    group, errCtx := errgroup.WithContext(ctx)
    ...
}

誤用在哪呢?答案是context會自動幫我們派生出新的context,除了需要設置超時一般不需要再次額外封裝,看源代碼:

// https://github.com/golang/sync/blob/master/errgroup/errgroup.go
 
// WithContext returns a new Group and an associated Context derived from ctx.
//
// The derived Context is canceled the first time a function passed to Go
// returns a non-nil error or the first time Wait returns, whichever occurs
// first.
func WithContext(ctx context.Context) (*Group, context.Context) {
	ctx, cancel := withCancelCause(ctx)
	return &Group{cancel: cancel}, ctx
}
 
// https://github.com/golang/sync/blob/master/errgroup/go120.go
 
func withCancelCause(parent context.Context) (context.Context, func(error)) {
	return context.WithCancelCause(parent)
}

多于的嵌套會浪費內存,以及會對性能帶來負面影響,尤其是需要從context里取出某些value的時候,因為取value是對一層層嵌套的context遞歸查找的,嵌套層數(shù)越多查找就有可能越慢。

不過前面也說到了,有一種情況是允許的,那就是對整個errgroup所有的協(xié)程設置超時:

func DoWork(ctx context.Context) {
    errCtx, cancel := context.WithTimeout(ctx, 10 * time.Second)
    defer cancel()
    group, errCtx := errgroup.WithContext(ctx)
    ...
}

目前想設置超時只能這樣做,所以這種算是特例。

Wait返回的時機

第二種誤用比第一種要常見些。主要是對errgroup的行為理解上有誤解。

這種誤解經常表現(xiàn)為:如果協(xié)程返回錯誤或者ctx的超時被觸發(fā),Wait方法就會立即返回。

這并不是事實。

先來看看Wait的文檔怎么說的:

Wait blocks until all function calls from the Go method have returned, then returns the first non-nil error (if any) from them.

Wait需要等到所有goroutine返回后它才會返回。哪怕超時了,context取消了也一樣,需要先等所有協(xié)程退出。再來看代碼:

// https://github.com/golang/sync/blob/master/errgroup/errgroup.go
 
func (g *Group) Wait() error {
	g.wg.Wait()
	if g.cancel != nil {
		g.cancel(g.err)
	}
	return g.err
}

可以看到確實需要先等所有協(xié)程返回。如果你觀察比較敏銳的話,其實能發(fā)現(xiàn)errgroup會對協(xié)程做包裝,會不會包裝的代碼里有什么辦法提前中止協(xié)程的執(zhí)行呢?還是來看代碼:

// https://github.com/golang/sync/blob/master/errgroup/errgroup.go
 
func (g *Group) Go(f func() error) {
	// 檢查當前協(xié)程是否可運行的代碼,先忽略
 
	g.wg.Add(1)
	go func() {
		defer g.done()  // 重點在這
 
		if err := f(); err != nil {
			g.errOnce.Do(func() {
				g.err = err
				if g.cancel != nil {
					g.cancel(g.err)
				}
			})
		}
	}()
}

注意那個defer,這意味著done只有在包裝的函數(shù)運行結束(在你自己的函數(shù)f運行完并設置了error以及取消了ctx之后)時才會執(zhí)行。

如果你自己的函數(shù)里不檢查超時和上下文是否被取消,那leak和卡死問題就要找上門來了,比如下面這樣的:

func main() {
    errCtx, cancel := context.WithTimeout(context.Background(), 1 * time.Second)
    defer cancel()
    group, errCtx := errgroup.WithContext(errCtx)
    group.Go(func () error {
        time.Sleep(10 * time.Second)
        fmt.Println("running")
        return nil
    })
    group.Go(func () error {
        return errors.New("error")
    })
    fmt.Println(group.Wait())
}

猜猜運行結果和執(zhí)行時間。答案是running\nerror\n,運行需要10秒以上。

這種誤用也很好識別,只要傳給Go方法的函數(shù)里沒有好好處理errCtx,那多半是有問題的。

不過要說句公道話,Go的參數(shù)形式不符合一般使用context的慣例,Wait的行為和其他能自主取消線程執(zhí)行的語言也不一樣造成了誤用,語言和接口設計得背一半鍋不能全賴用它的程序員。

SetLimit和死鎖

這種就更常見了,尤其發(fā)生在把errgroup當成普通協(xié)程池用的時候。

先來我最愛的猜謎游戲,下面的代碼運行結果是什么?

func main() {
    group, _ := errgroup.WithContext(context.Background())
    group.SetLimit(2) // 想法:只允許2個協(xié)程同時運行,但多個任務提交到“協(xié)程池”
    group.Go(func () error {
        fmt.Println("running 1")
        // 運行子任務
        group.Go(func () error {
            fmt.Println("sub running 1")
            return nil
        })
        group.Go(func () error {
            fmt.Println("sub running 2")
            return nil
        })
        return nil
    })
    group.Go(func () error {
        fmt.Println("running 2")
        // 運行子任務
        group.Go(func () error {
            fmt.Println("sub running 3")
            return nil
        })
        group.Go(func () error {
            fmt.Println("sub running 4")
            return nil
        })
        return nil
    })
    fmt.Println(group.Wait())
}

答案是會死鎖panic。而且是100%觸發(fā)。

我會詳細的解釋這是為什么,但在之前我要說一個重要的知識點:

SetLimit設置的不是同時在運行的協(xié)程數(shù)量,而是設置errgroup內最多同時能持有多少個協(xié)程,errgroup持有的協(xié)程可以在運行也可以在等待運行。

如果每個running的sub running只有一個,那么有小概率不會死鎖,所以我特地每組創(chuàng)建了兩個,原因沒那么復雜,看來后面的解釋之后可以自行推理。

下面來解釋,首先看SetLimit的代碼,一切是從這開始的:

// https://github.com/golang/sync/blob/master/errgroup/errgroup.go
 
// SetLimit limits the number of active goroutines in this group to at most n.
// A negative value indicates no limit.
//
// Any subsequent call to the Go method will block until it can add an active
// goroutine without exceeding the configured limit.
//
// The limit must not be modified while any goroutines in the group are active.
func (g *Group) SetLimit(n int) {
	if n < 0 {
		g.sem = nil
		return
	}
	if len(g.sem) != 0 {
		panic(fmt.Errorf("errgroup: modify limit while %v goroutines in the group are still active", len(g.sem)))
	}
	g.sem = make(chan token, n)
}

g.semchan strcut{}。做的事很簡單,如果參數(shù)大于0就按參數(shù)初始化一個長度為n的chan給g.sem,小于0就清空g.sem。如果你經驗比較豐富的話,已經可以看出來這是一個簡單的ticket pool模式了,這個模式在grpc里也有應用。

ticket pool模式的原理是設置一個固定大小為n的空chan,然后協(xié)程要運行的時候向這個chan寫入數(shù)據(jù),協(xié)程運行結束的時候從chan里把寫入的數(shù)據(jù)讀出(可能會讀到別人寫進去的,但只要遵循這個寫入讀出的順序就沒問題)。如果chan的寫入阻塞了,就說明已經有n個協(xié)程在運行了,新的協(xié)程需要等到有協(xié)程執(zhí)行完并讀出數(shù)據(jù)后才能繼續(xù)執(zhí)行;正常情況下讀出操作不會被阻塞。這個是限制goroutine數(shù)量的最常見的手段之一。根據(jù)寫入操作實在協(xié)程內部還是發(fā)起協(xié)程的調用者那里進行,這個模式還能分別控制“最大同時運行的goroutine數(shù)量”或“goroutine總數(shù)量”。其中goroutine的總數(shù)量 = 在運行的goroutine數(shù)量 + 其他等待運行goroutine的數(shù)量。

而errgroup屬于后者。還記得Go的代碼里我注釋掉的那部分吧,現(xiàn)在可以看了:

// https://github.com/golang/sync/blob/master/errgroup/errgroup.go
 
func (g *Group) Go(f func() error) {
	if g.sem != nil {
		g.sem <- token{} // token是struct{}
	}
 
	g.wg.Add(1)
	go func() {
		defer g.done()
 
		if err := f(); err != nil {
			// 設置錯誤值
		}
	}()
}
 
func (g *Group) done() {
	if g.sem != nil {
		<-g.sem // 從ticket pool里讀出
	}
	g.wg.Done()
}

進入Go的時候并沒有啟動協(xié)程,而是先檢查sem,如果有設置limit,就需要按操作ticket pool的流程先寫入數(shù)據(jù)。寫入成功才會創(chuàng)建協(xié)程,協(xié)程運行結束后把數(shù)據(jù)讀出。這樣限制了errgroup最大可以持有的協(xié)程數(shù)量,因為超過數(shù)量限制會阻塞住不創(chuàng)建新的協(xié)程。

Go完成sem的寫入并執(zhí)行go語句之前,errgroup并沒有“持有”go語句創(chuàng)建的這個協(xié)程。協(xié)程運行結束并把sem的數(shù)據(jù)讀出后,group將不會繼續(xù)“持有”這個協(xié)程。

問題就出在寫入那里。假設調度器是這樣運行我們的猜謎代碼的:

  • 先啟動running 1的協(xié)程,sem空位有2個,正常運行,running 1運行結束后它寫入的數(shù)據(jù)才會被讀出
  • 接著啟動running 2,sem還剩一個空位,沒問題,running 2運行結束后它寫入的數(shù)據(jù)才會被讀出
  • running 2先被執(zhí)行,于是準備創(chuàng)建sub running 3的協(xié)程
  • 這時sem沒空位了,創(chuàng)建sub running 3的Go阻塞
  • 調度器發(fā)現(xiàn)running 2被阻塞了,于是讓running 1執(zhí)行(假設而已,多核處理器上很可能是同時運行的)
  • running 1輸出后準備創(chuàng)建sub running 1的協(xié)程
  • sem還是滿的,Go又阻塞了
  • 調度器發(fā)現(xiàn)running 1和running 2都阻塞了,于是只能讓main goroutine執(zhí)行(這里忽略runtime自己的協(xié)程,因為不影響死鎖檢測結果)
  • main阻塞在Wait上,所有其他協(xié)程執(zhí)行完才能繼續(xù)執(zhí)行
  • 沒有能繼續(xù)運行下去的協(xié)程,全都阻塞了(注意是阻塞不是sleep),死鎖檢測發(fā)現(xiàn)這種情況,panic

我知道實際執(zhí)行順序肯定不一樣,但死鎖的原因一樣的:因為之前的協(xié)程沒有讓出ticket pool,后面的子任務需要向pool寫入,而前面占有pool的協(xié)程需要等子任務執(zhí)行完才會讓出pool。這是一個典型的循環(huán)依賴導致的死鎖,誘因是同一個errgroup的嵌套使用。

是什么導致了你踩坑呢?最大的可能是文檔里那個“active”。這個詞太模糊了,你可以發(fā)現(xiàn)它即能代指running又能代指runnable,還能兩個同時代指。這里因為下面還有一段話,所以可以根據(jù)上下文估摸著猜出active想代指的是所有被創(chuàng)建出來的協(xié)程不管它們在不在運行。但如果你只看了第一段話就先入為主放心大膽用的話,坑就來了。這樣的詞缺少足夠的上下文時連母語者都會覺得有二義性,更何況我們這些作為第二語言甚至第三語言的人。

而errgroup選擇限制goroutine總數(shù)量也是有原因的:只限制同時運行的goroutine的數(shù)量就沒法限制協(xié)程的總數(shù)量,協(xié)程雖然很輕量,但還是要占用內存以及花費cpu資源來調度的,不受控制很可能會產生災難性后果,比如一個不當心在循環(huán)里創(chuàng)建了數(shù)百萬個協(xié)程導致嚴重的內存占用和調度壓力,控制了總數(shù)量這類問題就可以避免。

幸運的是,這個誤用也很好識別,但凡有嵌套使用同一個errgroup的時候,就要警報大作了。

更幸運的是,如果你沒有嵌套調用,那么這個SetLimit不管設置成哪個數(shù)字,都能正常限制頂層的goroutine的數(shù)量(或者不做限制),它不能限制的是從頂層協(xié)程里嵌套調用派生出的子協(xié)程,只要不嵌套調用同一個group,什么問題的不會有。

前面兩種誤用都是該避免的,然而嵌套的errgroup雖然不多見但確實有用處,所以我也會提供寫簡單的解決方案以供參考。

第一種是設置一個足夠的limit數(shù)值,聰明人應該發(fā)現(xiàn)了,如果把limit設置成希望group里同時存在的協(xié)程的總數(shù)量(頂層+所有嵌套派生的),問題就能避免。這沒錯,但我不推薦,兩點原因:

  • 設置成總數(shù)后起不到限制同時運行的協(xié)程的數(shù)量,在go里控制同時運行的協(xié)程數(shù)量是個很麻煩的事,limit通常只能起到“上限”的作用,但如果上限設置大了就容易出現(xiàn)問題。比如你的系統(tǒng)只能同時運行3個協(xié)程,你還有別的任務占用了一個協(xié)程在運行,為了避免死鎖你設置了limit為4,這時候資源搶占和協(xié)程調度延遲都會明顯上升,出現(xiàn)這類情況你的系統(tǒng)就離崩潰只有一步之遙了。
  • 算這個數(shù)量很麻煩,上面的例子你可以很簡單算出是4,如果我再套一層或者加上幾個可以跳過Go調用的條件分支呢?而且limit設置多了是起不到限制goroutine數(shù)量的作用的,設少了會死鎖。
  • limit多半是個寫死的常量或者干脆是魔數(shù),那么下次協(xié)程的邏輯改了這個數(shù)字多半得跟著改,如果你算錯了或者忘記改了,那么你就慘了,死鎖就像個地雷一樣埋下了。

綜上,你應該用第二種方法:永遠不要嵌套使用同一個errgroup,真有嵌套需求也應該使用新的errgroup實例,這樣可以避免死鎖,也最符合當前需求的語義:

func main() {
    group, errCtx := errgroup.WithContext(context.Background())
    group.SetLimit(1) // 想法:只允許2個協(xié)程同時運行,但多個任務提交到“協(xié)程池”
    group.Go(func () error {
        fmt.Println("running 1")
        // 運行子任務
        // 新建一個errgroup,上下文使用外層group的
        subGroup, _ := errgroup.WithContext(errCtx)
        subGroup.SetLimit(1)
        subGroup.Go(func () error {
            fmt.Println("sub running 1")
            return nil
        })
        subGroup.Go(func () error {
            fmt.Println("sub running 2")
            return nil
        })
        fmt.Println(subGroup.Wait())
        return nil
    })
    group.Go(func () error {
        fmt.Println("running 2")
        // 運行子任務
        subGroup, _ := errgroup.WithContext(errCtx)
        subGroup.SetLimit(1)
        subGroup.Go(func () error {
            fmt.Println("sub running 3")
            return nil
        })
        subGroup.Go(func () error {
            fmt.Println("sub running 4")
            return nil
        })
        fmt.Println(subGroup.Wait())
        return nil
    })
    fmt.Println(group.Wait())
}

是的,現(xiàn)在所有l(wèi)imit設置成1也不會死鎖。因為沒有嵌套調用,因此也沒有資源間的循環(huán)依賴了。

當然還有終極方案:別把errgroup當成協(xié)程池,如果你有復雜功能依賴于協(xié)程池找個功能全面的真正的協(xié)程池比如ants之類的用。

對了。你問SetLimit傳0進去會發(fā)生什么,那當然是直接死鎖了。這也符合語義,因為你的group里不能有任何協(xié)程,這時候再調Go當然是不對的,死鎖panic也是應該的。所以傳0進去導致死鎖這不算坑,也算不上誤用。

總結

總結下上面三個誤用:

  • 傳遞有多余嵌套的context給errgroup
  • 在加入errgroup的協(xié)程里沒有正確處理context取消和超時
  • 嵌套使用同一個errgroup

已有的靜態(tài)分析工具不是很能識別這類問題,要么自己寫個能識別的,要么只能靠review把關了。

比較大眾的觀點認為go簡單易用,但實際上并不總是如此,有句話叫“Simple is not Easy”,go的使用者需要時刻為“大道至簡”付出相應的代價。

以上就是Golang中errgroup的常見誤用詳解的詳細內容,更多關于Go errgroup的資料請關注腳本之家其它相關文章!

相關文章

  • Golang通過SSH執(zhí)行交換機操作實現(xiàn)

    Golang通過SSH執(zhí)行交換機操作實現(xiàn)

    這篇文章主要介紹了Golang通過SSH執(zhí)行交換機操作實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2020-06-06
  • Go語言中你所不知道的位操作用法

    Go語言中你所不知道的位操作用法

    位運算可能在平常的編程中使用的并不多,但涉及到底層優(yōu)化,一些算法及源碼可能會經常遇見。下面這篇文章主要給大家介紹了關于Go語言中你所不知道的位操作用法的相關資料,文中通過示例代碼介紹的非常詳細,需要的朋友可以參考下。
    2017-12-12
  • golang time包的用法詳解

    golang time包的用法詳解

    這篇文章主要介紹了golang time包的用法詳解,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2019-04-04
  • Golang拾遺之自定義類型和方法集詳解

    Golang拾遺之自定義類型和方法集詳解

    golang拾遺主要是用來記錄一些遺忘了的、平時從沒注意過的golang相關知識。這篇文章主要整理了一下Golang如何自定義類型和方法集,需要的可以參考一下
    2023-02-02
  • Go語言標準輸入輸出庫的基本使用教程

    Go語言標準輸入輸出庫的基本使用教程

    輸入輸出在任何一門語言中都必須提供的一個功能,下面這篇文章主要給大家介紹了關于Go語言標準輸入輸出庫的基本使用,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下
    2022-02-02
  • Go中的Timer 和 Ticker詳解

    Go中的Timer 和 Ticker詳解

    在日常開發(fā)中,我們可能會遇到需要延遲執(zhí)行或周期性地執(zhí)行一些任務,這個時候就需要用到 Go 語言中的定時器,本文將會對這兩種定時器類型進行介紹,感興趣的朋友一起看看吧
    2024-07-07
  • Golang使用pprof檢查內存泄漏的全過程

    Golang使用pprof檢查內存泄漏的全過程

    pprof 是golang提供的一款分析工具,可以分析CPU,內存的使用情況,本篇文章關注它在分析內存泄漏方面的應用,本文給大家介紹了Golang使用pprof檢查內存泄漏的全過程,文中通過代碼給大家介紹的非常詳細,需要的朋友可以參考下
    2024-02-02
  • 手把手帶你走進Go語言之常量解析

    手把手帶你走進Go語言之常量解析

    這篇文章主要介紹了Go語言之常量解析,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2021-09-09
  • Go語言中更優(yōu)雅的錯誤處理

    Go語言中更優(yōu)雅的錯誤處理

    Go語言中的錯誤處理是一個被大家經常拿出來討論的話題(另外一個是泛型)。篇文章我們將討論一下如何在現(xiàn)行的 Golang 框架下提供更友好和優(yōu)雅的錯誤處理。需要的朋友們可以參考借鑒,下面來一起看看吧。
    2017-02-02
  • golang獲取客戶端ip的實現(xiàn)

    golang獲取客戶端ip的實現(xiàn)

    本文主要介紹了golang獲取客戶端ip的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2023-07-07

最新評論