Go sync.Once 的妙用
- 原文地址:https://blog.chuie.io/posts/synconce/
- 原文作者:Jason Chu
- 本文永久連結:https://github.com/gocn/translator/blob/master/2021/w34_the_underutilized_usefulness_of_sync_Once.md
- 譯者:張宇
- 校對:Cluas
如果你曾用過 Go 中的 goroutines,你也許會遇到幾個併發原語,如 sync.Mutex
, sync.WaitGroup
或是 sync.Map
,但是你聽說過 sync.Once
麼?
也許你聽說過,那 go 文件是怎麼描述它的呢?
Once 是隻執行一個操作的物件。
聽起來很簡單,它有什麼用處呢?
由於某些原因,sync.Once
的用法並沒有很好的文件記錄。在第一個.Do
中的操作執行完成前,將一直處於等待狀態,這使得在執行較昂貴的操作(通常快取在 map 中)時非常有用。
原生快取方式
假設你有一個熱門的網站,但它的後端 API 訪問不是很快,因此你決定將 API 結果通過 map 快取在記憶體中。以下是一個基本的解決方案:
package main
type QueryClient struct {
cache map[string][]byte
mutex *sync.Mutex
}
func (c *QueryClient) DoQuery(name string) []byte {
// 檢查結果是否已快取
c.mutex.Lock()
if cached, found := c.cache[name]; found {
c.mutex.Unlock()
return cached, nil
}
c.mutex.Unlock()
// 如果未快取則發出請求
resp, err := http.Get("https://upstream.api/?query=" + url.QueryEscape(name))
// 為簡潔起見,省略了錯誤處理和 resp.Body.Close
result, err := ioutil.ReadAll(resp)
// 將結果儲存在快取中
c.mutex.Lock()
c.cache[name] = result
c.mutex.Unlock()
return result
}
看起來不錯,對吧?
然而,如果有兩個 DoQuery
同時進行呼叫會發生什麼呢?競爭。兩方快取都無法命中,並且都會向 upstream.api
執行不必要的 HTTP 請求,而只有一個需要完成這個請求。
不美觀但更好的快取方式
我並沒有進行統計,但我認為大家解決這個問題的另外一種方式是使用 channel、context 或 mutex。在這個例子中,可以將上文程式碼調整為:
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 {
// 檢查操作是否已啟動
c.mutex.Lock()
if cached, found := c.cache[name]; found {
c.mutex.Unlock()
// 等待完成
<-cached.wait
return cached.data, nil
}
entry := &CacheEntry{
data: result,
wait: make(chan struct{}),
}
c.cache[name] = entry
c.mutex.Unlock()
// 如果未快取,則發出請求
resp, err := http.Get("https://upstream.api/?query=" + url.QueryEscape(name))
// 為簡潔起見,省略了錯誤處理和 resp.Body.Close
entry.data, err = ioutil.ReadAll(resp)
// 關閉 channel,傳遞操作完成訊號
// 立即返回
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 {
// 如果在快取中未找到,建立新的 entry
entry = &CacheEntry{
once: new(sync.Once),
}
c.cache[name] = entry
}
c.mutex.Unlock()
// 現在,當我們呼叫 .Do 時,如果有一個正在同步進行的操作
// 它將一直阻塞,直到完成(並填充 entry.data)
// 或者如果操作之前已經完成過一次
// 本次呼叫不會進行操作,也不會阻塞
entry.once.Do(func() {
resp, err := http.Get("https://upstream.api/?query=" + url.QueryEscape(name))
// 為簡潔起見,省略了錯誤處理和 resp.Body.Close
entry.data, err = ioutil.ReadAll(resp)
})
return entry.data
}
以上就是 sync.Once
的方案,和之前的示例很相似,但現在更容易理解(至少在我看來)。只有一個返回值,且程式碼自上而下,非常直觀,而不必像之前一樣對 entry.wait
channel 進行閱讀和理解。
進一步閱讀/其他注意事項
另一個類似於 sync.Once
的機制是 golang.org/x/sync/singleflight。singleflight
只會刪除正在進行中的請求中的重複請求(即不會持久化快取),但與 sync.Once
相比,singleflight
通過 context 實現起來可能更簡潔(通過使用 select
和 ctx.Done()
),並且在生產環境中,可以通過 context 取消這一點很重要。singleflight
實現的模式和 sync.Once
十分接近,但如果 map 中存有值,則會提前返回。
ianlancetaylor 建議結合 context 使用 sync.Once
,方式如下:
c := make(chan bool, 1)
go func() {
once.Do(f)
c <- true
}()
select {
case <-c:
case <-ctxt.Done():
return
}
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- Go channel 的妙用Go
- 【go 原始碼】sync.Once 詳解Go原始碼
- 【Go】我與sync.Once的愛恨糾纏Go
- 七. Go併發程式設計--sync.OnceGo程式設計
- 如何實現一個sync.Once
- 使用sync.Once實現高效的單例模式單例模式
- ActionChains 的妙用AI
- html <a>標籤的妙用HTML
- switch語句的妙用
- SQL中LIKE的妙用SQL
- v$session表的妙用Session
- V$session 表的妙用Session
- 二分的妙用
- 妙用javascriptJavaScript
- git 妙用Git
- js中的Boolean 的妙用JSBoolean
- Linux:“awk”命令的妙用Linux
- Javascript裝飾器的妙用JavaScript
- MacBook上的touchid妙用Mac
- Python之dict的妙用Python
- [轉] V$session 表的妙用Session
- V$session 表的妙用^_^(轉)Session
- with優化妙用優化
- KeyPath在Swift中的妙用Swift
- typescript:never與keyof的妙用TypeScript
- git rebase --onto 的奇妙用法Git
- C++中const的妙用C++
- vue-router中scrollBehavior的妙用Vue
- vue mixins和extends的妙用Vue
- source命令的一個妙用(轉)
- sync.Once和自己加鎖有什麼區別嗎?
- 妙用ConstraintLayout的Circular positioningAI
- 位運算子在JS中的妙用JS
- 食物在電子遊戲中的妙用遊戲
- CSS中content屬性的妙用CSS
- Web Worker在專案中的妙用Web
- Git命令的10大冷門妙用Git
- dbms_metadata.get_ddl的妙用