いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

Goメモ-414 (重複した呼び出しを抑制したい)(x/sync/singleflightパッケージ)

関連記事

GitHub - devlights/blog-summary: ブログ「いろいろ備忘録日記」のまとめ

概要

以下、自分用のメモです。忘れないようにここにメモメモ。。。

ヘビーな処理を行う関数なりメソッドなりがあって、それの呼び出しが特定の時間幅で重複して行われることがあったりします。

それらの重複した呼び出しを抑制して、最初に得られた結果をキャッシュして返すというのもよくありますね。

Go には、sync.OnceValue などがあるので、それで事足りることが多いのですが、singleflight というパッケージもあります。

singleflightパッケージは、重複した関数呼び出しを抑制するためのメカニズムを提供します。

このパッケージは、特に高価な操作や重複する操作が同時に複数のゴルーチンから要求される場合に有効です。

singleflightパッケージは、golang.org/x/sync/singleflightライブラリに含まれており、主に以下の機能を提供します。

  • 重複呼び出しの抑制:同じキーに対する複数のリクエストが同時に発生した場合、最初のリクエストが完了するまで他のリクエストを待機させ、結果を共有します。
  • 効率の向上:重複した操作を防ぐことで、サービスやデータベースへの不要な負荷を軽減します。
  • シンプルなAPI:Group型を使用して、重複する操作を管理します。

Cache Stampedeなどが発生する可能性がある部分などで利用出来ます。

sync.OnceValueなどと異なり、Group.Forget() が存在するのがちょっとした違いですかね。

サンプル

package main

import (
    "log"
    "sync"
    "time"

    "golang.org/x/sync/singleflight"
)

func init() {
    log.SetFlags(0)
}

func heavy(delay time.Duration, prefix string) {
    log.Printf("%s start", prefix)
    defer log.Printf("%s end  ", prefix)

    time.Sleep(delay)
}

func main() {
    // Group.Do() or Group.DoChan() を利用して
    // 重複する呼び出しが発生する箇所や処理負荷が高い操作などの呼び出しを
    // 抑制することが出来る。
    //
    // Cache Stampedeが発生する可能性がある部分には非常に有効です。

    const (
        KEY = "FUNC-GROUP-KEY"
    )

    var (
        grp     = &singleflight.Group{}
        ready   = make(chan struct{})
        results = make(chan (<-chan singleflight.Result))
        wg      = sync.WaitGroup{}
    )

    wg.Add(3)
    go func() {
        defer wg.Done()

        results <- grp.DoChan(KEY, func() (any, error) {
            <-ready
            heavy(3*time.Second, "func1")
            return "func1", nil
        })
    }()
    go func() {
        defer wg.Done()

        results <- grp.DoChan(KEY, func() (any, error) {
            <-ready
            heavy(1*time.Second, "func2")
            return "func2", nil
        })
    }()
    go func() {
        defer wg.Done()

        results <- grp.DoChan(KEY, func() (any, error) {
            <-ready
            heavy(2*time.Second, "func3")
            return "func3", nil
        })
    }()
    go func() {
        defer close(results)
        wg.Wait()
    }()

    // よーいドン
    close(ready)

    for ret := range results {
        log.Printf("%+v", <-ret)
    }

    // 更に追加呼び出し
    //   ただし、実行する前に Group.Forget() を呼び出して
    //   キーに紐づく結果を忘れさせてから実行
    grp.Forget(KEY)
    ret := grp.DoChan(KEY, func() (any, error) {
        heavy(1*time.Second, "func4")
        return "func4", nil
    })
    log.Printf("%+v", <-ret)
}

実行すると以下の様になります。

$ task
task: [build] go build -o app
task: [run] ./app
func1 start
func1 end  
{Val:func1 Err:<nil> Shared:true}
{Val:func1 Err:<nil> Shared:true}
{Val:func1 Err:<nil> Shared:true}
func4 start
func4 end  
{Val:func4 Err:<nil> Shared:false}

最初の3つの重複した呼び出しは抑制されて一つの結果が3回取得出来ていることが分かります。

4回目の呼び出しは、事前にキーに紐づく結果を忘れて( Forget メソッド)もらってから実行していますので、Sharedの値がfalseとなります。

参考情報

Goのおすすめ書籍


過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。