插件窝 干货文章 Go并发库singleflight源码解读:singleflight.Do函数的shared参数究竟代表什么?

Go并发库singleflight源码解读:singleflight.Do函数的shared参数究竟代表什么?

请求 结果 函数 并发 513    来源:    2025-03-16

singleflight 是 Go 语言中一个用于并发控制的库,主要用于防止对相同资源的重复请求。singleflight.Do 函数是 singleflight 库的核心函数之一,它的作用是确保在并发环境下,对相同资源的请求只会执行一次,其他并发的请求会等待第一个请求的结果并共享其结果。

singleflight.Do 函数签名

func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool)
  • key:用于标识请求的唯一键值。相同的 key 会被认为是相同的请求。
  • fn:实际执行请求的函数,返回请求的结果和可能的错误。
  • v:请求的结果。
  • err:请求过程中可能发生的错误。
  • shared:一个布尔值,表示结果是否被共享。

shared 参数的含义

shared 参数表示当前请求的结果是否是从其他并发请求中共享而来的。具体来说:

  • 如果 sharedtrue,表示当前请求的结果是从其他并发请求中共享的,也就是说当前请求并没有真正执行 fn 函数,而是直接使用了其他请求的结果。
  • 如果 sharedfalse,表示当前请求是第一个执行 fn 函数的请求,结果是由当前请求计算得出的。

源码解析

singleflight 的实现中,Do 函数会维护一个 call 结构体,用于存储每个 key 对应的请求状态。当多个并发请求使用相同的 key 调用 Do 函数时,singleflight 会确保只有一个请求真正执行 fn 函数,其他请求会等待这个请求的结果。

type call struct {
    wg  sync.WaitGroup
    val interface{}
    err error
}

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, bool) {
    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, true
    }
    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, false
}

代码流程

  1. 加锁Do 函数首先会加锁,确保并发安全。
  2. 检查是否有相同 key 的请求:如果已经有相同 key 的请求在处理中(即 c, ok := g.m[key]; oktrue),则当前请求会等待该请求的结果,并返回 shared = true
  3. 创建新的 call 结构体:如果没有相同 key 的请求在处理中,则创建一个新的 call 结构体,并将其添加到 g.m 中。
  4. 执行 fn 函数:当前请求会执行 fn 函数,并将结果存储在 call 结构体中。
  5. 释放锁并返回结果:最后,Do 函数会释放锁,并返回结果。如果是第一个请求,sharedfalse;如果是共享的结果,sharedtrue

总结

shared 参数的作用是告诉调用者当前请求的结果是否是从其他并发请求中共享而来的。如果 sharedtrue,则表示当前请求没有真正执行 fn 函数,而是直接使用了其他请求的结果。这在某些场景下非常有用,比如缓存穿透、防止重复计算等。