用處
保護下游,針對下游的同一批請求,只有一個負責去請求,其他等待結果;
例如:緩存更新能夠做到對同一個失效key的多個請求,只有一個請求執行對key的更新操作。
示例
func TestDoDupSuppress(t *testing.T) {
var g Group
c := make(chan string)
var calls int32
fn := func() (interface{}, error) {
atomic.AddInt32(&calls, 1)
return <-c, nil
}
const n = 10
var wg sync.WaitGroup
for i := 0; i < n; i++ {
wg.Add(1)
go func() { // n個協程同時調用了g.Do,fn中的邏輯只會被一個協程執行
v, err := g.Do("key", fn)
if err != nil {
t.Errorf("Do error: %v", err)
}
if v.(string) != "bar" {
t.Errorf("got %q; want %q", v, "bar")
}
wg.Done()
}()
}
time.Sleep(100 * time.Millisecond) // let goroutines above block
c <- "bar"
wg.Wait()
if got := atomic.LoadInt32(&calls); got != 1 {
t.Errorf("number of calls = %d; want 1", got)
}
}
- fn只被執行了一次 -> calls的值為1;
- 其他的攜程都能拿到fn執行的結果;
原理
map存儲每個key對應的call,每個call會被多個攜程同時調用。
一個call里邊有個waitgroup,第一個攜程去執行調用,其他攜程阻塞在wg上邊。 (關鍵就是這個wg)
- call的結構
type call struct {
wg sync.WaitGroup
val interface{} //最終返回的結果
err error
}
- map的結構
type Group struct {
mu sync.Mutex // protects m
m map[string]*call // lazily initialized
}
- 調用
func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) {
g.mu.Lock()
if g.m == nil {
g.m = make(map[string]*call)
}
if c, ok := g.m[key]; ok {
g.mu.Unlock()
c.wg.Wait() //其他的請求阻塞
return c.val, c.err
}
c := new(call)
c.wg.Add(1)
g.m[key] = c
g.mu.Unlock()
c.val, c.err = fn() //第一個去執行調用
c.wg.Done() //同一批都返回
g.mu.Lock()
delete(g.m, key)
g.mu.Unlock()
return c.val, c.err
}
- 實際使用的例子
ProductSku = singleflight.Group{}
skuList, err, shared := ProductSku.Do(strconv.FormatInt(productId, 10), func() (i interface{}, e error) {
return rpc.GetProductSku(ctx, productId, nil)
})