Go并发编程-SingleFlight
SingleFlight是Go提供的一个扩展并发原语。作用是当多个goroutine同时调用一个函数时,只有一个goroutine在真实调用,其他的goroutine在阻塞等待这个goroutine返回,然后直接将第一个goroutine的结果返回。这样可以减少并发时的调用量。在实际的开发中面对大并发请求的场景,如果请求都是读请求可以使用这个合并请求的方式来降低服务的压力。
简单用
同时开启1000个goroutine,通过count来控制耗时不同查询的请求。得到结果,使用了singleFlight非常有效的提高了效率
func TestSingleFlight(t *testing.T) {
time.AfterFunc(1*time.Second, func() {
atomic.AddInt32(&count, -count)
})
// 3.412698ms
// 1.002072002s
var (
wg sync.WaitGroup
now = time.Now()
n = 1000
sf = &singleflight.Group{}
)
for i := 0; i < n; i++ {
wg.Add(1)
go func() {
res, _ := singleFlightGetArticle(sf, 1)
//res, _ := getArticle(1)
if res != "article:1" {
panic("err")
}
wg.Done()
}()
}
wg.Wait()
fmt.Printf("同时发起 %d 次请求,耗时: %s", n, time.Since(now))
}
func getArticle(id int64) (string, error) {
atomic.AddInt32(&count, 1)
time.Sleep(time.Duration(count) * time.Millisecond)
return fmt.Sprintf("article:%d", id), nil
}
func singleFlightGetArticle(sf *singleflight.Group, id int64) (string, error) {
v, err, shared := sf.Do(fmt.Sprintf("%d", id), func() (interface{}, error) {
return getArticle(id)
})
fmt.Println("shared", shared)
return v.(string), err
}
看实现
// SingleFlight是使用结构体Group来实现的,总共有三个方法
type Group
//执行一个函数,第一个参数时key,相同的key有并发请求时会等待,第二个参数是一个无入参的函数,并返回结果或者error。do函数的返回值是第二个参数的函数的结果和错误,并且增加了是否返回给多个结果的标记
func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool)
//与Do类似,返回的结果使用Result结构体封装,并通过channel来返回
func (g *Group) DoChan(key string, fn func() (interface{}, error)) <-chan Result
//删除这个key,有并发的goroutine使用这个key再来请求时会重新调用该函数,而不是阻塞等待
func (g *Group) Forget(key string)
SingleFlight使用了一个call的辅助结构体,这个call就表示正在执行的fn函数的请求或者是已经执行完成的请求。Group表示SingleFlight
// 正在处理或者处理完成的请求
type call struct {
wg sync.WaitGroup
//处理完成后返回的结果
val interface{}
err error
//是否忘记key
forgotten bool
dups int
chans []chan<- Result
}
type Group struct {
mu sync.Mutex
m map[string]*call // 懒加载的方式,使用Group时不需要使用手动初始化
}
func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool) {
g.mu.Lock()
if g.m == nil { // 懒加载
g.m = make(map[string]*call)
}
if c, ok := g.m[key]; ok {
c.dups++
g.mu.Unlock()
c.wg.Wait() //包含相同的key则在此处阻塞,等待第一次调用的函数返回
if e, ok := c.err.(*panicError); ok {
panic(e)
} else if c.err == errGoexit {
runtime.Goexit()
}
return c.val, c.err, true
}
c := new(call)// 没有相同的key则是第一次调用
c.wg.Add(1)
g.m[key] = c
g.mu.Unlock()
g.doCall(c, key, fn) //调用函数
return c.val, c.err, c.dups > 0
}
//调用函数 采用了双层defer用来区分系统的panic和原函数的panic
//首先看第二个匿名函数
func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) {
normalReturn := false
recovered := false
defer func() {
//没有正常退出也没有recover,需要直接退出
if !normalReturn && !recovered {
c.err = errGoexit
}
c.wg.Done()
g.mu.Lock()
defer g.mu.Unlock()
//已经删除过key,则不用在重复删除了
if !c.forgotten {
delete(g.m, key)
}
// 避免channel死锁,需要确保这个panic无法恢复
if e, ok := c.err.(*panicError); ok {
if len(c.chans) > 0 {
go panic(e)
select {}
} else {
panic(e)
}
} else if c.err == errGoexit {
//直接退出
} else {
//正常退出
for _, ch := range c.chans {
ch <- Result{c.val, c.err, c.dups > 0}
}
}
}()
func() {
defer func() {
//如果normalReturn为false则表示原函数调用panic了,这里将其recover掉,然后在上面的defer重新panic
if !normalReturn {
if r := recover(); r != nil {
c.err = newPanicError(r)
}
}
}()
//调用原函数,如果原函数panic,就不会执行normalReturn = true。
c.val, c.err = fn()
normalReturn = true
}()
//表示原函数panic了,并且进行了recover住了不是直接runtime exit
if !normalReturn {
recovered = true
}
}
应用场景
SingleFlight常用于缓存系统,可以有效的解决缓存击穿的问题。有大量的请求到来时,缓存的key过期失效了。会直接打在数据库上。使用SingleFlight当有大量请求来到时会只有一个请求落到库上,其他的并发请求会共享这个结果,因为是缓存查询,不用考虑幂等问题。知名的groupcache中使用了SingleFlight来实现的。
// groupcache只使用了SingleFlight中的Do函数
type Group struct{
loadGroup flightGroup
}
func(g *Group) load(ctx context.Context, key string, dest Sink)(interface{}, error){
viewi, err := g.loadGroup.Do(key, func() (interface{}, error) {
// 从cache, peer, local尝试查询cache
return value, nil
})
if err == nil {
value = viewi.(ByteView) }
return
}