七叶笔记 » golang编程 » sync.Once 你真的会用吗?

sync.Once 你真的会用吗?

如果您曾经在 Go 中使用过 goroutine,那么您可能会遇到几个并发原语。也许 sync.Mutex sync.WaitGroup 也许 sync.Map ,但你听说过 sync.Once 吗?

也许你有,但godoc 怎么说呢?

听起来很简单,那么它有什么用呢?

好吧,出于某种原因,这不是特别有据可查的,但是 a sync.Once 等到 第一个内部的执行 .Do 完成。这在执行通常会缓存在map中的相对昂贵的操作时非常有用。

原始缓存

例如,假设您有一个热门网站,该网站访问的后端 API 不是特别快,因此您决定使用map将 API 结果缓存在内存中。一个简单的解决方案可能如下所示:

 package main

type QueryClient struct {
    cache map[string][]byte
    mutex *sync.Mutex
}

func (c *QueryClient) DoQuery(name string) []byte {
    // Check if the result is already cached.
    c.mutex.Lock()
    if cached, found := c.cache[name]; found {
        c.mutex.Unlock()
        return cached, nil
    }
    c.mutex.Unlock()

    // Make the request if it's uncached.
    resp, err := http.Get("#34; + url.QueryEscape(name))
    // Error handling and resp.Body.Close omitted for brevity.
    result, err := ioutil.ReadAll(resp)

    // Store the result in the cache.
    c.mutex.Lock()
    c.cache[name] = result
    c.mutex.Unlock()

    return result
}  

看起来不错,对吧?

那么如果有两个调用 DoQuery 同时发生会发生什么?调用会竞争,两者都不会看到缓存被填充,并且两者都会 upstream.api 不必要地执行 HTTP 请求,而此时只有一个人需要完成它。

丑陋但更好的缓存

我没有这方面的统计数据,但我认为人们解决这个问题的一种方法是使用通道、上下文或互斥锁。例如你可以把它变成:

 package main

type CacheEntry struct {
    data []byte
    wait <-chan struct{}
}

type QueryClient struct {
    cache map[string]*CacheEntry
    mutex *sync.Mutex
}

func (c *QueryClient) DoQuery(name string) []byte {
    // Check if the operation has already been started.
    c.mutex.Lock()
    if cached, found := c.cache[name]; found {
        c.mutex.Unlock()
        // Wait for it to complete.
        <-cached.wait
        return cached.data, nil
    }

    entry := &CacheEntry{
        data: result,
        wait: make(chan struct{}),
    }
    c.cache[name] = entry
    c.mutex.Unlock()

    // Make the request if it's uncached.
    resp, err := http.Get("#34; + url.QueryEscape(name))
    // Error handling and resp.Body.Close omitted for brevity
    entry.data, err = ioutil.ReadAll(resp)

    // Signal that the operation is complete, receiving on closed channels
    // returns immediately.
    close(entry.wait)

    return entry.data
}  

这很好,但代码的可读性受到了打击。目前还不清楚发生了什么 cached.wait ,不同情况下的操作流程也不是很直观。

使用 sync.Once

让我们尝试应用于 sync.Once 此:

 package main

type CacheEntry struct {
    data []byte
    once *sync.Once
}

type QueryClient struct {
    cache map[string]*CacheEntry
    mutex *sync.Mutex
}

func (c *QueryClient) DoQuery(name string) []byte {
    c.mutex.Lock()
    entry, found := c.cache[name]
    if !found {
        // Create a new entry if one does not exist already.
        entry = &CacheEntry{
            once: new(sync.Once),
        }
        c.cache[name] = entry
    }
    c.mutex.Unlock()

    // Now when we invoke `.Do`, if there is an on-going simultaneous operation,
    // it will block until it has completed (and `entry.data` is populated).
    // Or if the operation has already completed once before,
    // this call is a no-op and doesn't block.
    entry.once.Do(func() {
        resp, err := http.Get("#34; + url.QueryEscape(name))
        // Error handling and resp.Body.Close omitted for brevity
        entry.data, err = ioutil.ReadAll(resp)
    })

    return entry.data
}  

就是这样。这与前面的示例实现相同,但现在更容易理解(至少在我看来)。只有一个返回,代码从上到下直观地流动,无需 entry.wait 像以前一样阅读和理解通道发生了什么。

其他注意事项

另一种类似的机制 sync.Once 是golang.org/x/sync/singleflight。但是, singleflight 仅对进行中的请求进行重复数据删除(即不持久缓存)。 singleflight sync.Once (通过使用 a select ctx.Done() )相比,使用上下文实现可能更清晰,在生产环境中,这对于能够使用上下文取消可能很重要。模式与 singleflight 非常相似, sync.Once 但如果map中存在值,会提前返回。

ianlancetaylor建议使用以下方式让 sync.Once 与上下文一起使用:

 c := make(chan bool, 1)
go func() {
    once.Do(f)
    c <- true
}()
select {
case <-c:
case <-ctxt.Done():
    return
}  

相关文章