Go并发系列:3扩展同步原语-3.3 SingleFlight

103 阅读3分钟

3.3 SingleFlight

在并发编程中,有时我们希望避免多次对同一资源的重复请求,从而节省系统资源和提高效率。Go 语言中的 singleflight 包提供了一种解决方案,能够在多 goroutine 并发请求同一资源时进行合并,确保相同请求只执行一次。下面我们详细介绍 SingleFlight 的概念、使用方法及示例。

3.3.1 什么是SingleFlight

SingleFlight 是 Go 语言 golang.org/x/sync/singleflight 包中的一种同步机制,用于合并并发的相同请求,确保相同的请求在同一时间只执行一次。SingleFlight 的核心思想是,如果有多个 goroutine 同时请求同一个资源,SingleFlight 会合并这些请求,只执行一次实际的请求操作,并将结果返回给所有请求的 goroutine。

SingleFlight 主要有以下特点:

  • 请求合并:将并发的相同请求合并为一个,避免重复计算和资源浪费。
  • 结果共享:将实际请求的结果共享给所有发起相同请求的 goroutine。
  • 错误处理:支持错误传播,如果实际请求出错,所有请求方都能收到相同的错误信息。

3.3.2 SingleFlight的使用方法

使用 SingleFlight 非常简单。首先,创建一个 Group 实例,然后通过 Do 方法发起请求,SingleFlight 会处理请求的合并和结果共享。以下是基本使用示例:

package main

import (
    "fmt"
    "golang.org/x/sync/singleflight"
    "sync"
    "time"
)

func expensiveOperation(key string) (string, error) {
    time.Sleep(2 * time.Second)
    return fmt.Sprintf("Result for %s", key), nil
}

func main() {
    var g singleflight.Group
    var wg sync.WaitGroup

    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            v, err, shared := g.Do("myKey", func() (interface{}, error) {
                return expensiveOperation("myKey")
            })
            if err != nil {
                fmt.Printf("Goroutine %d error: %v\n", i, err)
                return
            }
            fmt.Printf("Goroutine %d got result: %s (shared: %t)\n", i, v.(string), shared)
        }(i)
    }

    wg.Wait()
}

在这个例子中,多个 goroutine 同时请求 expensiveOperation,由于使用了 SingleFlight,它们的请求会被合并,只执行一次实际的请求操作,结果会共享给所有发起请求的 goroutine。

3.3.3 SingleFlight的应用场景

SingleFlight 适用于以下场景:

  1. 缓存失效:在缓存失效时,避免多个请求同时访问数据库或外部 API。
  2. 去重请求:避免重复请求同一资源,例如防止短时间内多次点击按钮导致的重复请求。
  3. 资源密集型操作:对于资源密集型操作(如复杂计算、网络请求),使用 SingleFlight 可以减少系统负载。

3.3.4 示例代码

以下是一个使用 SingleFlight 实现缓存失效处理的示例:

package main

import (
    "fmt"
    "golang.org/x/sync/singleflight"
    "sync"
    "time"
)

type Cache struct {
    mu    sync.Mutex
    data  map[string]string
    group singleflight.Group
}

func NewCache() *Cache {
    return &Cache{
        data: make(map[string]string),
    }
}

func (c *Cache) Get(key string) (string, error) {
    c.mu.Lock()
    value, ok := c.data[key]
    c.mu.Unlock()
    if ok {
        return value, nil
    }

    v, err, _ := c.group.Do(key, func() (interface{}, error) {
        return expensiveOperation(key)
    })
    if err != nil {
        return "", err
    }

    c.mu.Lock()
    c.data[key] = v.(string)
    c.mu.Unlock()
    return v.(string), nil
}

func expensiveOperation(key string) (string, error) {
    time.Sleep(2 * time.Second)
    return fmt.Sprintf("Result for %s", key), nil
}

func main() {
    cache := NewCache()
    var wg sync.WaitGroup

    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            value, err := cache.Get("myKey")
            if err != nil {
                fmt.Printf("Goroutine %d error: %v\n", i, err)
                return
            }
            fmt.Printf("Goroutine %d got value: %s\n", i, value)
        }(i)
    }

    wg.Wait()
}

在这个示例中,我们定义了一个 Cache 结构体,使用 SingleFlight 处理缓存失效时的并发请求,避免多次执行 expensiveOperation

结论

SingleFlight 提供了一种优雅的方式来合并并发的相同请求,避免重复计算和资源浪费。在实际应用中,通过使用 SingleFlight,可以有效提高系统的性能和资源利用率。在接下来的章节中,我们将继续探讨其他同步原语和并发编程技巧,帮助您更好地掌握 Go 的并发编程。