這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
Goroutines
- 模型:和其他goroutine在共用的地址空間中並發執行的函數
- 資源消耗: 初始時非常小的棧開銷,之後隨著需求在堆上增減記憶體
- 建立和銷毀: go 關鍵字表示建立一個新的goroutine(注意不會馬上執行,而是放在調度的隊列中等待調度), 函數運行結束後,goroutine自動銷毀
goroutine才是golang的優勢之處,簡單,輕量的並行存取模型。
Channel
- 資料類型的一種,類似訊息佇列,便於不同goroutine間通訊。
- 可單可雙通道,可以包含各種類型的資料;也可以分帶buffer和不帶buffer的
- 從空的channel中讀取資料會阻塞(關閉的管道不會阻塞),同樣往滿的channel中寫資料也會阻塞
- channel不像檔案、網路通訊端那樣,close不會釋放資源,只是不再接收更多訊息,因而不需要通過close來釋放channel資源;但是如果有range loop的,需要close掉,要不range loop會block住
- 如果往關閉的channel中寫入資料,則會panic;如果是讀資料的,先讀取管道中多餘的資料,之後都會取得零值
- 如果事先不知道有多少個channel,可以用reflect.Select來選擇
Sync package
There’s a disconnect between the concurrency primitives that Go, and the expectations of those who try it.
golang提供了非常簡便的並行存取模型,但並發編程仍然不容易。
本文
先從'go'關鍵字開始。
package mainimport ( "fmt" "log" "net/http")func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hello, GopherCon SG") }) go func() { if err := http.ListenAndServe(":8080", nil); err != nil { log.Fatal(err) } }() for {}}hello world web2.0 版本
上面的代碼有什麼問題?
for {}
for{}
是個死迴圈,會一直佔用cpu,導致cpu空轉。
怎麼解決呢?
for { runtime.Gosched() }
讓出CPU,但這種做法還是會佔用cpu,沒有解決根本問題。有更好點的辦法,用select{}
替代for{}
,空select{}
語句會一直阻塞。
上面的樣本僅僅為了示範一些小問題,不會正式地使用,下面這種寫法,才是我們經常使用的正確樣本:
package mainimport ( "fmt" "log" "net/http")func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hello, GopherCon SG") }) if err := http.ListenAndServe(":8080", nil); err != nil { log.Fatal(err) }}
If you have to wait for the result of an operation, it’s easier to do it yourself.
- 第一個建議:如果需要等待某個操作的結果,不需要再建立goroutine運行這個操作,同時阻塞外層goroutine
既然最大的特點是並發,當然不能錯過並發的樣本了:
func restore(repos []string) error { errChan := make(chan error, 1) sem := make(chan int, 4) // four jobs at once var wg sync.WaitGroup wg.Add(len(repos)) for _, repo := range repos { sem <- 1 go func() { defer func() { wg.Done() <-sem }() if err := fetch(repo); err != nil { errChan <- err } }() } wg.Wait() close(sem) close(errChan) return <-errChan}這個是gb-vendor早期的一個版本,並發的擷取依賴資源
仔細觀察下,覺得代碼怎麼樣,能只出哪些問題?
首先來看下這一對代碼塊:
defer func() { wg.Done() <-sem}()和這段:wg.Wait()close(sem)
close(sem)在wg.Wait()之後,wg.Wait()在wg.Done()之後,但是並不能保證在<-sem之後發生,也就是說close(sem)和<-sem誰先誰後是沒有保證的。那麼有可能導致panic麼?
參考最上面關於channel的介紹:從關閉了的channel中讀取資料,(如果有)先取出管道中的資料,之後會直接返回零值,不會阻塞。
簡單的修改下defer,可以讓執行順序變得清晰:
func restore(repos []string) error { errChan := make(chan error, 1) sem := make(chan int, 4) // four jobs at once var wg sync.WaitGroup wg.Add(len(repos)) for _, repo := range repos { sem <- 1 go func() { defer wg.Done() if err := fetch(repo); err != nil { errChan <- err } <-sem }() } wg.Wait() close(sem) close(errChan) return <-errChan}
- 第二建議:鎖和訊號量的釋放順序與他們擷取的順序相反。
有點類型多層鎖,內層的鎖先釋放,而後才是外層。
Why close(sem)?
channel不像檔案、網路通訊端那樣,close不會釋放資源,只是不再接收更多訊息,因而不需要通過close來釋放channel資源;但是如果有range loop的,需要close掉,要不range loop會block住.
這裡沒有channel的range loop,因而可以刪除close(sem)
再來看看sem是如何使用的
sem是為了在任何時候,僅有有限的fetch操作在運行。仔細觀察下前面的代碼,有什麼疑問嗎?
代碼僅僅保證了不超過4個goroutine在運行,而不是4個fetch操作正在運行,再體會下兩者的卻別。
前面的代碼只保證不超過4個goroutine再運行,當第五repo時,會阻塞for迴圈,等待之前某個goroutine執行完了之後,再建立一個goroutine(不會馬上執行),相對來說效率低下。
還有一種是將所需要的goroutine放入調度池,然後直接運行:
func restore(repos []string) error { errChan := make(chan error, 1) sem := make(chan int, 4) // four jobs at once var wg sync.WaitGroup wg.Add(len(repos)) for _, repo := range repos { go func() { defer wg.Done() sem <- 1 if err := fetch(repo); err != nil { errChan <- err } <-sem }() } wg.Wait() close(errChan) return <-errChan}
將 sem <- 1放入go func裡面,所有的goroutine都會建立好,並馬上執行.
bug都搞定了?
回到上面的代碼,注意 for .. range 和fetch(repo) 代碼塊,看出什麼問題了嗎?
有兩個問題:
1.goroutine中的變數repo會隨著每次迭代而改變,可能導致所有的fetch操作都是抓取最後一次的值
2.如果對變數repo同時有讀寫操作的話,會引起競爭
怎麼處理呢?給匿名方法添加參數:
func restore(repos []string) error { errChan := make(chan error, 1) sem := make(chan int, 4) // four jobs at once var wg sync.WaitGroup wg.Add(len(repos)) for i := range repos { go func(repo string ) { defer wg.Done() sem <- 1 if err := fetch(repo); err != nil { errChan <- err } <-sem }(repos[i]) } wg.Wait() close(errChan) return <-errChan}
- 建議4:避免在goroutine中直接使用外部變數,最好以參數的方式傳遞
最後一個了吧?
wait, one more bug
回到上面代碼,仔細觀察errChan和fetch error的處理,估計打死都看不出問題吧?
給點小提示,如果超過一個error,會出現什麼情況?
close(errChan)依賴於wg.Wait()先執行,wg.Wait()依賴於wg.Done()先執行,wg.Done又依賴於errChan <-err先執行,但errChan的buffer只有1,goroutine卻有四個。但超過一個error時,boom...,errChan <- err 操作阻塞了,形成死結。
解決辦法,errChan的buffer等於repos的個數: errChan := make(chan error, len(repos))
- 最後一條建議:當你建立goroutine時,需要知道什麼時候,怎麼樣退出這個goroutine
golang提供的並行存取模型很簡單,但是用好並發還需要掌握各種常見模式和情境,而不僅僅是語言方面的知識
個人部落格:blog.duomila.club
參考資料:
https://dave.cheney.net/paste/concurrency-made-easy.pdf