golang並發,簡之道

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

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 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

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.