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
- 有讀寫鎖、寫鎖,atomic,waitgroup
There’s a disconnect between the concurrency primitives that Go, and the expectations of those who try it.
golang提供了非常簡便的并發模型,但并發編程仍然不容易。
正文
先從'go'關鍵字開始。
package main
import (
"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 main
import (
"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