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

Go底層之singleflight包原理分析

 更新時(shí)間:2025年06月25日 16:46:16   作者:在成都搬磚的鴨鴨  
這篇文章主要介紹了Go底層之singleflight包原理,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教

背景

在處理同一時(shí)刻接口的并發(fā)請(qǐng)求時(shí),常見的有這幾種情況:

一個(gè)請(qǐng)求正在執(zhí)行,相同的其它請(qǐng)求等待順序執(zhí)行,使用互斥鎖就能完成、一個(gè)請(qǐng)求正在執(zhí)行,相同的其它請(qǐng)求都丟棄、一個(gè)請(qǐng)求正在執(zhí)行,相同的其它請(qǐng)求等待拿取相同的結(jié)果。

使用singleflight包就能達(dá)到一個(gè)請(qǐng)求正在執(zhí)行,相同的其它請(qǐng)求過來等待第一個(gè)請(qǐng)求執(zhí)行完,然后共享第一個(gè)請(qǐng)求的結(jié)果,在處理并發(fā)場(chǎng)景時(shí)非常好用。

下載

go get -u golang.org/x/sync/singleflight

原理解釋

  • singleflight底層結(jié)構(gòu):
type Group struct {
	mu sync.Mutex       //保護(hù)map對(duì)象m的并發(fā)安全
	m  map[string]*call //key-請(qǐng)求的唯一標(biāo)識(shí),val-請(qǐng)求唯一標(biāo)識(shí)對(duì)應(yīng)要執(zhí)行的函數(shù)
}
  • call底層結(jié)構(gòu):
type call struct {
	wg sync.WaitGroup //用來阻塞相同標(biāo)識(shí)對(duì)應(yīng)的請(qǐng)求中的第一個(gè)請(qǐng)求之外的請(qǐng)求
	val interface{} //第一個(gè)請(qǐng)求的執(zhí)行結(jié)果
	err error //第一個(gè)請(qǐng)求返回的錯(cuò)誤
	dups  int //第一個(gè)請(qǐng)求之外的其它請(qǐng)求數(shù)
	chans []chan<- Result //請(qǐng)求結(jié)果寫入通道
}
  • 關(guān)鍵函數(shù):
//
// Do
//  @Description: 一個(gè)唯一標(biāo)識(shí)對(duì)應(yīng)的請(qǐng)求在執(zhí)行過程中,相同唯一標(biāo)識(shí)對(duì)應(yīng)的請(qǐng)求會(huì)被阻塞,等待第一個(gè)請(qǐng)求執(zhí)行完并共享結(jié)果
//  @receiver g 
//  @param key 請(qǐng)求唯一標(biāo)識(shí)
//  @param fn 請(qǐng)求要執(zhí)行的函數(shù)
//  @return v 請(qǐng)求要執(zhí)行函數(shù)返回的結(jié)果
//  @return err 請(qǐng)求要執(zhí)行的函數(shù)返回的錯(cuò)誤
//  @return shared 是否有多個(gè)請(qǐng)求共享結(jié)果
//
func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool) {
	g.mu.Lock() //保護(hù)map對(duì)象m的并發(fā)安全
	if g.m == nil {
		g.m = make(map[string]*call) //初始化m對(duì)象
	}
	if c, ok := g.m[key]; ok { //map中key存在說明這個(gè)key對(duì)應(yīng)的請(qǐng)求正在執(zhí)行中,這次請(qǐng)求不是第一個(gè)請(qǐng)求
		c.dups++ //等待共享結(jié)果數(shù)+1
		g.mu.Unlock()
		c.wg.Wait() //阻塞等待第一個(gè)請(qǐng)求執(zhí)行完

		if e, ok := c.err.(*panicError); ok {
			panic(e)
		} else if c.err == errGoexit {
			runtime.Goexit()
		}
		return c.val, c.err, true //返回第一個(gè)請(qǐng)求的執(zhí)行結(jié)果
	}
    
    //第一個(gè)請(qǐng)求的處理邏輯
	c := new(call)
	c.wg.Add(1) //計(jì)數(shù)+1
	g.m[key] = c //唯一標(biāo)識(shí)關(guān)聯(lián)對(duì)應(yīng)的函數(shù)對(duì)象
	g.mu.Unlock()

	g.doCall(c, key, fn) //執(zhí)行請(qǐng)求對(duì)應(yīng)的函數(shù)
	return c.val, c.err, c.dups > 0 //返回執(zhí)行結(jié)果
}

上面Do函數(shù)中g(shù).doCall函數(shù)也需要大概理解一下,就是會(huì)將key對(duì)應(yīng)函數(shù)的執(zhí)行結(jié)果寫的call對(duì)象c里,然后清空wg計(jì)數(shù),相同key對(duì)應(yīng)的其它請(qǐng)求就會(huì)跳出c.wg.Wait()阻塞,直接從call對(duì)象c中讀取第一個(gè)請(qǐng)求的執(zhí)行結(jié)果和錯(cuò)誤信息并返回,源碼如下:

func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) {
	normalReturn := false
	recovered := false
	
	defer func() { //fn函數(shù)執(zhí)行完之后執(zhí)行
		if !normalReturn && !recovered {
			c.err = errGoexit
		}

		g.mu.Lock()
		defer g.mu.Unlock()
		c.wg.Done() //釋放計(jì)數(shù)
		if g.m[key] == c {
			delete(g.m, key) //刪除此次請(qǐng)求的唯一標(biāo)識(shí)相關(guān)信息
		}

		if e, ok := c.err.(*panicError); ok {
			if len(c.chans) > 0 {
				go panic(e)
				select {} 
			} else {
				panic(e)
			}
		} else if c.err == errGoexit {
			// Already in the process of goexit, no need to call again
		} else {
			for _, ch := range c.chans { //執(zhí)行結(jié)果寫入通道
				ch <- Result{c.val, c.err, c.dups > 0}
			}
		}
	}()

	func() {
		defer func() {
			if !normalReturn {
				if r := recover(); r != nil {
					c.err = newPanicError(r)
				}
			}
		}()

		c.val, c.err = fn() //執(zhí)行fn函數(shù)
		normalReturn = true
	}()

	if !normalReturn {
		recovered = true
	}
}

singleflight中還提供了與Do函數(shù)功能相同的函數(shù)DoChan函數(shù),唯一區(qū)別就是將請(qǐng)求對(duì)應(yīng)的函數(shù)執(zhí)行結(jié)果放到通道中進(jìn)行返回,這兩函數(shù)一個(gè)用于同步場(chǎng)景,一個(gè)用于異步場(chǎng)景。還有一個(gè)Forget函數(shù):

func (g *Group) Forget(key string) {
	g.mu.Lock()
	delete(g.m, key) //刪除map中的key,相同key對(duì)應(yīng)請(qǐng)求進(jìn)來會(huì)重新執(zhí)行,不等待第一個(gè)key對(duì)應(yīng)請(qǐng)求的執(zhí)行結(jié)果
	g.mu.Unlock()
}

代碼示例

  • 示例如下:
func main() {
	var singleFlight singleflight.Group //初始化一個(gè)單次執(zhí)行對(duì)象
	var count uint64                    //用于測(cè)試是否被修改

	//為了并發(fā)執(zhí)行,這里測(cè)試唯一標(biāo)識(shí)都為xxx
	go func() {
		val1, _, shared1 := singleFlight.Do("xxx", func() (interface{}, error) {
			logger.Info("first count +1")

			atomic.AddUint64(&count, 1) //第一次執(zhí)行,將count+1

			time.Sleep(5 * time.Second) //增加第一次執(zhí)行時(shí)間

			return count, nil
		})

		//打印第一次執(zhí)行結(jié)果
		logger.Info("first count info", zap.Any("val1", val1), zap.Bool("shared1", shared1), zap.Uint64("count", count))
	}()

	time.Sleep(2 * time.Second) //為了防止下面的Do函數(shù)先執(zhí)行

	val2, _, shared2 := singleFlight.Do("xxx", func() (interface{}, error) {
		logger.Info("second count +1")

		atomic.AddUint64(&count, 1) //第2次執(zhí)行count+1

		return count, nil
	})

	//打印第二次執(zhí)行結(jié)果
	logger.Info("second count info", zap.Any("val2", val2), zap.Bool("shared2", shared2), zap.Uint64("count", count))
}
  • 控制臺(tái)輸出:
$ go run ./singlefight_demo/main.go
[2025-01-09 17:08:11.169] | INFO  | Goroutine:6  | [singlefight_demo/main.go:19] | first count +1
[2025-01-09 17:08:16.261] | INFO  | Goroutine:6  | [singlefight_demo/main.go:28] | first count info | {"val1": 1, "shared1": true, "count": 1}
[2025-01-09 17:08:16.261] | INFO  | Goroutine:1  | [singlefight_demo/main.go:41] | second count info | {"val2": 1, "shared2": true, "count": 1}

總結(jié)

看singleflight原碼之后,要實(shí)現(xiàn)一個(gè)請(qǐng)求正在執(zhí)行,相同的其它請(qǐng)求進(jìn)來時(shí)直接報(bào)錯(cuò)的功能也很簡(jiǎn)單,將singleflight中等待第一個(gè)請(qǐng)求的邏輯改為直接返回錯(cuò)誤就可以。

以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。

相關(guān)文章

  • 如何使用?Go?和?Excelize?構(gòu)建電子表格

    如何使用?Go?和?Excelize?構(gòu)建電子表格

    這篇文章主要介紹了如何使用Go和Excelize構(gòu)建電子表格,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下
    2022-09-09
  • golang flag包的使用教程

    golang flag包的使用教程

    golang 的 flag 包是用于處理命令行參數(shù)的工具包,我們可以基于這個(gè)包來開發(fā)自定義的命令行工具,下面小編就來為大家介紹一下flag包的具體使用吧
    2023-09-09
  • Go Mongox輕松實(shí)現(xiàn)MongoDB的時(shí)間字段自動(dòng)填充

    Go Mongox輕松實(shí)現(xiàn)MongoDB的時(shí)間字段自動(dòng)填充

    這篇文章主要為大家詳細(xì)介紹了Go語言如何使用 mongox 庫(kù),在插入和更新數(shù)據(jù)時(shí)自動(dòng)填充時(shí)間字段,從而提升開發(fā)效率并減少重復(fù)代碼,需要的可以參考下
    2025-02-02
  • 在Go中構(gòu)建并發(fā)TCP服務(wù)器

    在Go中構(gòu)建并發(fā)TCP服務(wù)器

    今天小編就為大家分享一篇關(guān)于在Go中構(gòu)建并發(fā)TCP服務(wù)器的文章,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來看看吧
    2018-10-10
  • 實(shí)現(xiàn)像php一樣方便的go ORM數(shù)據(jù)庫(kù)操作示例詳解

    實(shí)現(xiàn)像php一樣方便的go ORM數(shù)據(jù)庫(kù)操作示例詳解

    這篇文章主要為大家介紹了實(shí)現(xiàn)像php一樣方便的go ORM數(shù)據(jù)庫(kù)操作示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-12-12
  • GOLANG使用Context實(shí)現(xiàn)傳值、超時(shí)和取消的方法

    GOLANG使用Context實(shí)現(xiàn)傳值、超時(shí)和取消的方法

    這篇文章主要介紹了GOLANG使用Context實(shí)現(xiàn)傳值、超時(shí)和取消的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2019-01-01
  • 淺析Go語言如何在select語句中實(shí)現(xiàn)優(yōu)先級(jí)

    淺析Go語言如何在select語句中實(shí)現(xiàn)優(yōu)先級(jí)

    這篇文章主要為大家詳細(xì)介紹了Go語言如何在select語句中實(shí)現(xiàn)優(yōu)先級(jí),文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下
    2024-03-03
  • 解決golang 關(guān)于全局變量的坑

    解決golang 關(guān)于全局變量的坑

    這篇文章主要介紹了解決golang 關(guān)于全局變量的坑,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧
    2021-05-05
  • golang的強(qiáng)制類型轉(zhuǎn)換實(shí)現(xiàn)

    golang的強(qiáng)制類型轉(zhuǎn)換實(shí)現(xiàn)

    這篇文章主要介紹了golang的強(qiáng)制類型轉(zhuǎn)換實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2021-02-02
  • Golang泛型與反射的應(yīng)用詳解

    Golang泛型與反射的應(yīng)用詳解

    如果我想編寫一個(gè)可以輸出任何給定類型的切片并且不使用反射的打印功能,則可以使用新的泛型語法。文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2022-06-06

最新評(píng)論