Go 高階併發
- 原文地址:https://encore.dev/blog/advanced-go-concurrency
- 原文作者:André Eriksson
- 譯文出處:https://encore.dev/blog
- 本文永久連結:https://github.com/gocn/translator/blob/master/2020/w1_advanced_go_concurrency.md
- 譯者:咔嘰咔嘰
- 校對者:fivezh
如果你曾經使用過 Go 一段時間,那麼你可能瞭解一些 Go 中的併發原語:
-
go
關鍵字用來生成 goroutines -
channel
用於 goroutines 之間通訊 -
context
用於傳播取消 -
sync
和sync/atomic
包用於低階別的原語,例如互斥鎖和記憶體的原子操作
這些語言特性和包組合在一起,為構建高併發的應用程式提供了豐富的工具集。你可能還沒有發現在擴充套件庫 golang.org/x/sync 中,提供了一系列更高階別的併發原語。我們將在本文中來談談這些內容。
singleflight 包
正如文件中所描述,這個包提供了一個重複函式呼叫抑制的機制。
如果你正在處理計算量大(也可能僅僅是慢,比如網路訪問)的使用者請求時,這個包就很有用。例如,你的資料庫中包含每個城市的天氣資訊,並且你想將這些資料以 API 的形式提供服務。在某些情況下,可能同時有多個使用者想查詢同一城市的天氣。
在這種場景下,如果你只查詢一次資料庫然後將結果共享給所有等待的請求,這樣不是更好嗎?這就是 singleflight
提供的功能。
在使用的時候,我們需要要建立一個 singleflight.Group
。它需要在所有請求中共享才能工作。然後將緩慢或者開銷大的操作包裝到 group.Do(key, fn)
的呼叫中。對同一個 key
的多個併發請求將僅呼叫 fn
一次,並且將 fn
的結果返回給所有呼叫者。
實際中的使用如下:
package weather
type Info struct {
TempC, TempF int // temperature in Celsius and Farenheit
Conditions string // "sunny", "snowing", etc
}
var group singleflight.Group
func City(city string) (*Info, error) {
results, err, _ := group.Do(city, func() (interface{}, error) {
info, err := fetchWeatherFromDB(city) // 慢操作
return info, err
})
if err != nil {
return nil, fmt.Errorf("weather.City %s: %w", city, err)
}
return results.(*Info), nil
}
需要注意的是,我們傳遞給 group.Do
的閉包必須返回 (interface{}, error)
才能和 Go 型別系統一起使用。上面的例子中忽略了 group.Do
的第三個返回值,該值是用來表示結果是否在多個呼叫方之間共享。
如果需要檢視更多完整的例子,可以檢視 Encore Playground 中的程式碼。
errgroup 包
另一個有用的包是 errgroup package。它和 sync.WaitGroup
比較相似,但是會將任務返回的錯誤回傳給阻塞的呼叫方。
當你有多個等待的操作,但又想知道它們是否都已經成功完成時,這個包就很有用。還是以上面的天氣為例,假如你要一次查詢多個城市的天氣,並且要確保其中所有的查詢都成功返回。
首先定義一個 errgroup.Group
,然後為每個城市都使用 group.Go(fn func() error)
方法。該方法會生成一個 goroutine 來執行這個任務。當生成你想執行的所有任務時,使用 group.Wait()
等待它們完成。需要注意和 sync.WaitGroup
有一點不同的是,該方法會返回錯誤。當且僅當所有任務都返回 nil
時,才會返回一個 nil
錯誤。
實際中的使用如下:
func Cities(cities ...string) ([]*Info, error) {
var g errgroup.Group
var mu sync.Mutex
res := make([]*Info, len(cities)) // res[i] corresponds to cities[i]
for i, city := range cities {
i, city := i, city // 為下面的閉包建立區域性變數
g.Go(func() error {
info, err := City(city)
mu.Lock()
res[i] = info
mu.Unlock()
return err
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return res, nil
}
這裡我們使用一個 res
切片來儲存每個 goroutine 執行的結果。儘管上面的程式碼沒有使用 mu
互斥鎖也是執行緒安全的,但是每個 goroutine 都是在切片中自己的位置寫入結果,因此我們不得不使用一個切片,以防程式碼變化。
限制併發
上面的程式碼將同時查詢給定城市的天氣資訊。如果城市數量比較少,那還不錯,但是如果城市數量很多,可能會導致效能問題。在這種情況下,就應該引入限制併發了。
在 Go 中使用 semaphores 訊號量讓實現限制併發變得非常簡單。訊號量是你學習電腦科學中可能已經遇到過的併發原語,如果沒有遇到也不用擔心。你可以出於多種目的來使用訊號量,但是這裡我們只使用它來追蹤執行中的任務的數量,並阻塞直到有空間可以執行其他任務。
在 Go 中,我們可以使用 channel
來實現訊號量的功能。如果我們一次需要最多執行 10 個任務,則需要建立一個容量為 10 的 channel
:semaphore := make(chan struct{}, 10)
。你可以想象它為一個可以容納 10 個球的管道。
如果想執行一個新的任務,我們只需要給 channel
傳送一個值:semaphore <- struct{}{}
,如果已經有很多工在執行的話,將會阻塞。這類似於將一個球推入管道,如果管道已滿,則需要等待直到有空間為止。
當通過 <-semaphore
能從該 channel
中取出一個值時,這表示一個任務完成了。這類似於在管道另一端拿出一個球,這將為塞入下一個球提供了空間。
如描述一樣,我們修改後的 Cities
程式碼如下:
func Cities(cities ...string) ([]*Info, error) {
var g errgroup.Group
var mu sync.Mutex
res := make([]*Info, len(cities)) // res[i] corresponds to cities[i]
sem := make(chan struct{}, 10)
for i, city := range cities {
i, city := i, city // create locals for closure below
sem <- struct{}{}
g.Go(func() error {
info, err := City(city)
mu.Lock()
res[i] = info
mu.Unlock()
<-sem
return err
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return res, nil
}
加許可權制併發
最後,當你想要限制併發的時候,並不是所有任務優先順序都一樣。在這種情況下,我們消耗的資源將依據高、低優先順序任務的分佈以及它們如何開始執行而發生變化。
在這種場景下使用加許可權制併發是一種不錯的解決方式。它的工作原理很簡單:我們不需要為同時執行的任務數量做預估,而是為每個任務提供一個 "cost",並從訊號量中獲取和釋放它。
我們不再使用 channel
來做這件事,因為我們需要立即獲取並釋放 "cost"。幸運的是,"擴充套件庫" golang.org/x/sync/sempahore 實現了加權訊號量。
sem <- struct{}{}
操作叫 "獲取",<-sem
操作叫 "釋放"。你可能會注意到 semaphore.Acquire
方法會返回錯誤,那是因為它可以和 context
包一起使用來控制提前結束。在這個例子中,我們將忽略它。
實際上,天氣查詢的例子比較簡單,不適用加權訊號量,但是為了簡單起見,我們假設 cost
變數隨城市名稱長度而變化。然後,我們修改如下:
func Cities(cities ...string) ([]*Info, error) {
ctx := context.TODO() // 需要的時候,可以用 context 替換
var g errgroup.Group
var mu sync.Mutex
res := make([]*Info, len(cities)) // res[i] 對應 cities[i]
sem := semaphore.NewWeighted(100) // 併發處理 100 個字元
for i, city := range cities {
i, city := i, city // 為閉包建立區域性變數
cost := int64(len(city))
if err := sem.Acquire(ctx, cost); err != nil {
break
}
g.Go(func() error {
info, err := City(city)
mu.Lock()
res[i] = info
mu.Unlock()
sem.Release(cost)
return err
})
}
if err := g.Wait(); err != nil {
return nil, err
} else if err := ctx.Err(); err != nil {
return nil, err
}
return res, nil
}
結論
上面的例子展示了在 Go 中通過微調來實現需要的併發模式是多麼簡單。
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- Go高階併發 09 | 同步原語:sync 包讓你對併發控制得心應手Go
- 【Go進階—併發程式設計】MutexGo程式設計Mutex
- 【Go進階—併發程式設計】ContextGo程式設計Context
- 【Go進階—併發程式設計】WaitGroupGo程式設計AI
- [分散式][高併發]高併發架構分散式架構
- Go併發原理Go
- Go 併發 -- 通道Go
- go 併發 mapGo
- go併發 - channelGo
- Go 併發操作Go
- 唯品會招聘 GO高階開發工程師Go工程師
- 構建高併發&高可用&安全的IT系統-高併發部分
- OpenResty高併發REST
- 高併發(鎖)
- Go高效併發 11 | 併發模式:Go 語言中即學即用的高效併發模式Go模式
- Android 高階面試-3:Java、同步和併發相關Android面試Java
- Go 併發 -- 協程Go
- GO語言併發Go
- 什麼是高併發,怎麼解決高併發
- Go 併發程式設計 - 併發安全(二)Go程式設計
- 【羅輯思維招聘】 Go高階研發工程師Go工程師
- 高階併發:Akka Actors和JavaEE7的EJB比較Java
- 第09章 Go語言併發,Golang併發Golang
- 高併發技術
- Java 高併發思路Java
- 高併發架構架構
- Java高併發綜合Java
- 網站高併發網站
- Go 筆記之併發Go筆記
- 學習 Go併發模型Go模型
- go併發-工作池模式Go模式
- GO-併發技術Go
- Go 併發程式設計Go程式設計
- Pandas高階教程之:Dataframe的合併
- Twitter 高併發高可用架構架構
- 【深圳-區塊鏈-Go】火幣網深圳公司招聘Go高階開發工程師區塊鏈Go工程師
- 深圳威新軟體園, 招聘GO高階開發工程師Go工程師
- 高併發優化方向優化