golang并發,簡之道

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,739評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,634評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,653評論 0 377
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,063評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,835評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,235評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,315評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,459評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,000評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,819評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,004評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,560評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,257評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,676評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,937評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,717評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,003評論 2 374