我可以在 Go 中使用特定值进行锁定吗?

Can I lock using specific values in Go?

In answering another question I wrote a little struct using sync.Map 缓存 API 个请求。

type PostManager struct {
    sync.Map
}

func (pc PostManager) Fetch(id int) Post {
    post, ok := pc.Load(id)
    if ok {
        fmt.Printf("Using cached post %v\n", id)
        return post.(Post)
    }
    fmt.Printf("Fetching post %v\n", id)
    post = pc.fetchPost(id)
    pc.Store(id, post)

    return post.(Post)
}

不幸的是,如果两个 goroutines 同时获取相同的未缓存 Post,两者都会发出请求。

var postManager PostManager

wg.Add(3)

var firstPost Post
var secondPost Post
var secondPostAgain Post

go func() {
    // Fetches and caches 1
    firstPost = postManager.Fetch(1)
    defer wg.Done()
}()

go func() {
    // Fetches and caches 2
    secondPost = postManager.Fetch(2)
    defer wg.Done()
}()

go func() {
    // Also fetches and caches 2
    secondPostAgain = postManager.Fetch(2)
    defer wg.Done()
}()

wg.Wait()

我需要确保当同时提取同一 ID 时,只允许一个实际发出请求。另一个必须等​​待并将使用缓存的 Post。但也不要锁定不同 ID 的提取。

在上面的例子中,我希望只有一次调用 pc.fetchPost(1)pc.fetchPost(2),并且它们应该是同时的。

Link to the full code.

您可以使用两张地图来做到这一点,一张保存缓存的值,另一张保存正在获取的值。您还需要将锁保留更长的时间,这样就不需要保留 sync.Map,一个普通的地图就可以了。这样的东西应该可以工作(未经测试):

type PostManager struct {
    sync.Mutex
    cached map[int]Post
    loading map[int]chan struct{}
}

您需要处理以下加载失败的情况:

// Need to pass pointer pc
func (pc *PostManager) Fetch(id int) Post {
    pc.Lock()
    post, ok:=pc.cached[id]
    if ok {
        pc.Unlock()
        return post
    }
    // See if it is being loaded
    loading, ok:=pc.loading[id]
    if ok {
       // Wait for the loading to complete
       pc.Unlock()
       <-loading
       // Reload
       pc.Lock()
       post,ok:=pc.cached[id]
       // Maybe you need to handle the case where loading failed?
       pc.Unlock()
       return post
    }
    // load it
    loading=make(chan struct{})
    pc.loading[id]=loading
    pc.Unlock()
    post = pc.fetchPost(id)
    pc.Lock()
    pc.cached[id]=post
    delete(pc.loading,id)
    pc.Unlock()
    close(loading)
    return post
}

如果抓取已经在进行中,似乎可以使用第二张地图等待。

type PostManager struct {
    sync.Map
    q sync.Map
}

func (pc *PostManager) Fetch(id int) Post {
    post, ok := pc.Load(id)
    if ok {
        fmt.Printf("Using cached post %v\n", id)
        return post.(Post)
    }
    fmt.Printf("Fetching post %v\n", id)
    if c, loaded := pc.q.LoadOrStore(id, make(chan struct{})); !loaded {
        post = pc.fetchPost(id)
        pc.Store(id, post)
        close(c.(chan struct{}))
    } else {
        <-c.(chan struct{})
        post,_ = pc.Load(id)
    }
    return post.(Post)
}

或者,更复杂一点,使用相同的地图;-)

func (pc *PostManager) Fetch(id int) Post {
    p, ok := pc.Load(id)

    if !ok {
        fmt.Printf("Fetching post %v\n", id)
        if p, ok = pc.LoadOrStore(id, make(chan struct{})); !ok {
            fetched = pc.fetchPost(id)
            pc.Store(id, fetched)
            close(p.(chan struct{}))
            return fetched
        }
    }

    if cached, ok := p.(Post); ok {
        fmt.Printf("Using cached post %v\n", id)
        return cached
    }

    fmt.Printf("Wating for cached post %v\n", id)
    <-p.(chan struct{})
    return pc.Fetch(id)
}

golang.org/x/sync/singleflight package正是为此而写的。

请注意,所有缓存访问都应该在传递给 Do 的回调函数内发生。在代码中,您 link 在您的评论中进行了外部查找;这有点违背了目的。

此外,您必须使用指向 singleflight.Group 的指针。那是您的数据竞赛的来源,请兽医指出:

./foo.go:41:10: fetchPost passes lock by value: command-line-arguments.PostManager contains golang.org/x/sync/singleflight.Group contains sync.Mutex

我会这样写(操场上的完整示例:https://play.golang.org/p/2hE721uA88S):

import (
    "strconv"
    "sync"

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

type PostManager struct {
    sf    *singleflight.Group
    cache *sync.Map
}

func (pc *PostManager) Fetch(id int) Post {
    x, _, _ := pc.sf.Do(strconv.Itoa(id), func() (interface{}, error) {
        post, ok := pc.cache.Load(id)
        if !ok {
            post = pc.fetchPost(id)
            pc.cache.Store(id, post)
        }

        return post, nil
    })

    return x.(Post)
}