【Go并发编程】SingleFlight 源码阅读

149 阅读2分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 24 天,点击查看活动详情

SingleFlight

SingleFlight 是 Go 语言 sync 包提供的一个功能,用于避免重复请求并发执行,同时还能减轻后端的压力。

SingleFlight 的主要思想是,在进行并发请求时,只执行一次请求,其他的请求则等待这个请求返回结果,然后重用结果。这就相当于将一些重复的请求合并为一次请求,可以减轻后端服务器的压力,同时也避免了一些竞态条件。

SingleFlight 主要包含以下三个方法:

image.png

  • Do:执行请求,并返回结果;对于同一个 key,在同一时间只有一个在执行,同一个 key 并发的请求会等待。第一个执行的请求返回的结果,就是它的返回结果。
  • DoChan:类似 Do 方法,只不过是返回一个 chan,等 fn 函数执行完,产生了结果以后,就能从这个 chan 中接收这个结果。
  • Forget:告诉 Group 忘记这个 key。这样一来,之后这个 key 请求会执行 f,而不是等待前一个未完成的 fn 函数的结果。

SingleFlight 的实现过程主要是利用了 Go 语言中的 sync.Mutex 和 sync.Cond,使用互斥锁和条件变量进行并发控制,保证请求的唯一性和并发安全性。

下面是一个简单的 SingleFlight 的示例代码,其中使用了 sync 包提供的 SingleFlight 对象,然后通过 Do 方法来执行请求:

func main() {
   var group singleflight.Group
   var wg sync.WaitGroup
   for i := 0; i < 10; i++ {
      wg.Add(1)
      go func(i int) {
         defer wg.Done()
         val, err, _ := group.Do("key", func() (interface{}, error) {
            time.Sleep(time.Nanosecond)
            return i, nil
         })
         fmt.Printf("result:%v error:%v\n", val, err)
      }(i)
   }
   wg.Wait()
}

输出:

result:0 error:<nil>
result:0 error:<nil>
result:0 error:<nil>
result:0 error:<nil>
result:0 error:<nil>
result:0 error:<nil>
result:0 error:<nil>
result:0 error:<nil>
result:0 error:<nil>
result:0 error:<nil>

这里函数执行需要消耗时间,在上一次Do完成前再次调用的Do不会重复执行,Do完成后向这一段时间调用Do的返回同样的值。相当于同一时刻只有一个Do会执行,把并发请求转化为单线程的。

sync.Once 主要是用在单次初始化场景中,而 SingleFlight 主要用在合并并发请求的场景中,尤其是缓存场景。

源码阅读

// call 就代表正在执行 fn 函数的请求或者是已经执行完的请求。
type call struct {
   wg sync.WaitGroup
   // 这个字段代表处理完的值,在waitgroup完成之前只会写一次
   val   interface{}
   err   error
   dups  int
   chans []chan<- Result
}

// group代表一个singleflight对象
type Group struct {
   mu sync.Mutex       // protects m
   m  map[string]*call // lazily initialized
}
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)
   }
   //如果已经存在相同的key
   if c, ok := g.m[key]; ok {
      c.dups++
      g.mu.Unlock()
      //等待这个key的第一个请求完成
      c.wg.Wait()

      if e, ok := c.err.(*panicError); ok {
         panic(e)
      } else if c.err == errGoexit {
         runtime.Goexit()
      }
      //使用第一个key的请求结果
      return c.val, c.err, true
   }
   // 第一个请求,创建一个call
   c := new(call)
   c.wg.Add(1)
   // 加入到key map中
   g.m[key] = c
   g.mu.Unlock()
   // 调用方法
   g.doCall(c, key, fn)
   return c.val, c.err, c.dups > 0
}

应用场景

  • 合并相同请求
  • 解决缓存击穿