Goroutine就像變了心的女友一樣回不去了,當執行Goroutine發生error時,不就都沒有辦法知道嗎?
只好靠elk或是大軍的特異功能
其實Golang有一個叫errgroup的套件可以做到
直接引用golang.org/x/sync/errgroup 不需要額外go get套件,使用方式跟一般的Goroutine微不同,
以下為實作的code
package main
import (
"fmt"
"net/http"
"golang.org/x/sync/errgroup"
)
func main() {
//宣告一個errgroup.Group
var g errgroup.Group
var urls = []string{
"http://www.golang.org/",
"http://www.google.com/",
"http://www.somestupidname.com/",
}
for _, url := range urls {
// 小心,這邊是上篇中有提到的closure的踩雷點
url := url // https://golang.org/doc/faq#closures_and_goroutines
//不是直接用go func(),而是用func (g *Group) Go(f func() error)
g.Go(func() error {
// Fetch the URL.
resp, err := http.Get(url)
if err == nil {
resp.Body.Close()
}
//有發現嗎,這邊有回傳error
return err
})
}
// 用errgroup除了攔劫error外,一樣也可以做到waitgroup的事情,
if err := g.Wait(); err == nil {
fmt.Println("Successfully fetched all URLs.")
}
}
errgroup也支援使用context,如果要做上下層context管理的話,可以透過errgroup.WithContext進行Goroutine
g, ctx := errgroup.WithContext(context.Background())
競爭危害(race hazard)又名競態條件、競爭條件(race condition),它旨在描述一個系統或者進程的輸出依賴於不受控制的事件出現順序或者出現時機。此詞源自於兩個訊號試著彼此競爭,來影響誰先輸出。
舉例來說,如果電腦中的兩個行程同時試圖修改一個共享記憶體的內容,在沒有並行控制的情況下,最後的結果依賴於兩個行程的執行順序與時機。而且如果發生了並行存取衝突,則最後的結果是不正確的。
競爭危害常見於不良設計的電子系統,尤其是邏輯電路。但它們在軟體中也比較常見,尤其是有採用多執行緒技術的軟體。
from wiki 競爭危害
用上面寫的code來示範一次
var respBody string
for _, url := range urls {
url := url
g.Go(func() error {
// Fetch the URL.
resp, err := http.Get(url)
if err == nil {
defer resp.Body.Close()
body1, err1 := ioutil.ReadAll(resp.Body)
if err1 != nil {
return err1
}
respBody = string(body1)
}
return err
})
上面的code執行後會發生什麼事情?
答案是回傳值每次都不一樣或是噴出data race的錯誤,因為Goroutine都對respBody這變數進行存取
要如何解決呢?
有下面幾種方式
sync.Mutex簡單說就是把變數鎖上不讓其他goroutine存取到此變數,等正在使用的goroutine做完解鎖後就換另一個goroutine使用,就是排隊輪流用啦
var respBody string
//宣告互斥鎖
var m sync.Mutex
for _, url := range urls {
url := url
g.Go(func() error {
// Fetch the URL.
resp, err := http.Get(url)
if err == nil {
defer resp.Body.Close()
body1, err1 := ioutil.ReadAll(resp.Body)
if err1 != nil {
return err1
}
//上鎖
m.Lock()
respBody = string(body1)
//存取完變數後解鎖
m.Unlock()
}
return err
})
}
sync.RWMutex是個讀寫互斥鎖(multiple readers, single writer lock),多個讀取單筆寫入,
如果數個Goroutine同時對一個會有寫入行為的變數進行讀取的話,沒Lock的話會造成data race,
但是如果每次讀取都要lock的話就會讓效能整個低落,所以才要改用RWMutex,
RWMutex雖然還是要讀取時也要加上RWLock()與RWUnlock(),但是如果沒有寫入行為時就不會進行鎖定,
使用方式很簡單
//原本的sync.Mutex
var m sync.Mutex
//換成sync.RWMutex
var rwm sync.RWMutex
//原本上鎖
m.Lock()
respBody = string(body1)
m.Unlock()
//改成
m.RLock()
respBody = string(body1)
m.RUnlock()
golang本身有提供一個語法進行確認是否有data race的情形,但是有個缺點就是如果是屬於很難得踩到的data race,這個語法不一定偵測出來
go run -race main.go
官方文章有提到map非原子操作,所以如果用goroutine去讀寫map的話,會發生data race的情形,
以下方例子來說,雖然goroutine操作的map對應的key值是不同的,但是還是一樣會噴出data race的錯誤,
就算加上RWMutex也是一樣
func main() {
var g errgroup.Group
mp := make(map[string]string)
g.Go(func() error {
mp["A"] = "BAD"
return nil
})
g.Go(func() error {
mp["B"] = "GOOD"
return nil
})
if err := g.Wait(); err != nil {
fmt.Println("goroutine err:", err)
}
}
//Found 1 data race(s)
func main() {
var g errgroup.Group
var m sync.Map
g.Go(func() error {
m.Store("A", "BAD")
return nil
})
g.Go(func() error {
m.Store("B", "GOOD")
return nil
})
if err := g.Wait(); err != nil {
fmt.Println("goroutine err:", err)
}
v, ok := m.Load("A")
if ok {
fmt.Println("it's an existing key,value is ", v)
} else {
fmt.Println("it's an unknown key")
}
v, ok = m.Load("B")
if ok {
fmt.Println("it's an existing key,value is ", v)
} else {
fmt.Println("it's an unknown key")
}
}
執行結果,不再噴data race惹
it's an existing key,value is BAD
it's an existing key,value is GOOD