【问题标题】:Can I lock using specific values in Go?我可以在 Go 中使用特定值锁定吗?
【发布时间】:2020-02-12 23:50:59
【问题描述】:

In answering another question 我写了一个小结构体,使用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)
}

不幸的是,如果两个 goroutine 同时获取同一个未缓存的 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.

【问题讨论】:

  • "包 singleflight 提供了重复的函数调用抑制机制。" godoc.org/golang.org/x/sync/singleflight
  • @Peter 这很棒,也是迄今为止最简单的解决方案。如果您发布答案,我会接受。但是,go run --race 在 Do() 处显示了数据竞争。 Code here.

标签: go mutex


【解决方案1】:

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

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

此外,您必须使用指向 singleflight.Group 的指针。这就是你的数据竞赛的来源,请去看看:

./foo.go:41:10: fetchPost 按值传递锁:command-line-arguments.PostManager 包含 golang.org/x/sync/singleflight.Group 包含 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)
}

【讨论】:

    【解决方案2】:

    看起来可以使用第二个地图来等待,如果已经在进行获取。

    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)
    }
    

    【讨论】:

    • 这不会阻止多个 goroutine 启动 fetch。
    • 为什么?它对于不同的主题是完全并发的。但是,对于相同的主题 ID,只有一次 fetchPost 调用。
    • 感谢您的回答,但都不起作用。这是two mapssingle map 的完整代码。
    • 我的错,一定有“func (pc *PostManager) Fetch(id int) Post”。它必须通过引用访问 pc。
    【解决方案3】:

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

    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
    }
    

    【讨论】:

    • 谢谢。我无法对所有地图使用单个互斥锁来完成这项工作,它会死锁或竞争。我确实使用两个 sync.Maps 使它工作,但我偶尔会参加比赛。 Here's the code.
    • 是的,我看到了比赛,现在应该修好了。我同意单程飞行是最好的解决方案。
    • 谢谢,但go run --race 继续报告竞争状况。我很困惑。 Code.
    • fetchPost 必须获得一个指针接收器。没有它,你就是在复制互斥体。
    猜你喜欢
    • 1970-01-01
    • 2011-04-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-11-14
    • 1970-01-01
    相关资源
    最近更新 更多