GoLang協程
學習golang也有一段時間了,這裡講一下自己對golang協程的使用理解,golang很多人都知道,畢竟有個好爹Google,提起golang和其它語言最大區別莫過於goroutine,
也就是go的協程,先來一個demo
package mainfunc say(s string) { for i := 0; i < 5; i++ { println(s) }}func main() { go say("Hello") go say("World")}
go 啟動協程的方式就是使用關鍵字 go,後面一般接一個函數或者類似下面的匿名函數的寫法
go func() { for i := 0; i < 5; i++ { println(i) }}()
當然如果你運行上面第一段代碼,你會發現什麼結果都沒有,what???
這至少說明你代碼寫的沒問題,當你使用go啟動協程之後,這2個函數就被切換到協程裡面執行了,但是這時候主線程結束了,這2個協程還沒來得及執行就掛了!
聰明的小夥伴會想到,那我主線程先睡眠1s等一等?Yes, 在main代碼塊最後一行加入:
time.Sleep(time.Second*1) # 表示睡眠1s
你會發現可以列印出5個Hello 和 5個World,多次運行你會發現Hello 和 World 的順序不是固定的,這進一步說明了一個問題,那就是多個協程是同時執行的
不過睡眠這種做法肯定是不靠譜的,go 內建一個WaitGroup可以解決這個問題, 代碼如下:
package mainimport ( "sync")var wg sync.WaitGroupfunc say(s string) { for i := 0; i < 5; i++ { println(s) } wg.Done()}func main() { wg.Add(2) go say("Hello") go say("World") wg.Wait()}
簡單說明一下用法,var 是聲明了一個全域變數 wg,類型是sync.WaitGroup,wg.add(2) 是說我有2個goroutine需要執行,
wg.Done 相當於 wg.Add(-1) 意思就是我這個協程執行完了。wg.Wait() 就是告訴主線程要等一下,等他們2個都執行完再退出。
舉個例子,你有一個需求是從3個庫取不同的資料匯總處理,同步代碼的寫法就是查3次庫,但是這3次查詢必須按順序執行,大部分程式設計語言的代碼執行順序都是從上到下,假如一個
查詢耗時1s,3個查詢就是3s,但是使用協程你可以讓這3個查詢同時進行,也就是1s就可以搞定(前提是資料庫跟得上)。還有一個更有實際用途的例子就是用來寫爬蟲。
不過為了更好的使用協程,你可能還得瞭解一下管道 Chanel,go 裡面的管道是協程之間通訊的渠道,上面的例子裡面我們是直接列印出來結果,假如現在的需求是把輸出結果返回到主線程呢?
package mainimport ( "sync")var wg sync.WaitGroupfunc say(s string, c chan string) { for i := 0; i < 5; i++ { c <- s } wg.Done()}func main() { wg.Add(2) ch := make(chan string) // 執行個體化一個管道 go say("Hello", ch) go say("World", ch) for { println(<-ch) //迴圈從管道取資料 } wg.Wait()}
簡單說明一下,這裡就是執行個體化了一個管道,go啟動的協程同時向這個2個管道輸出資料,主線程使用了一個for迴圈從管道裡面取資料,其實就是一個生產者和消費者模式,和redis隊列有點像
值得一說的是 World 和 Hello 進入管道的順序是不固定的,可能大家實驗的時候發現好像是固定的,那是因為電腦跑的太快了,你把迴圈資料放大,或者在裡面加個睡眠再看看
但是這個程式是有bug的,在程式的啟動並執行最後會輸出這樣的結果:
fatal error: all goroutines are asleep - deadlock!
報錯資訊的提示意思是所有的協程都睡眠了,程式監測到死結!為什麼會這樣呢?我是這樣理解的,go的管道預設是阻塞的(假如你不設定緩衝的話),你那邊放一個,我這頭才能取一個,
如果你那邊放了東西這邊沒人取,程式就會一直等下去,死結了,同時,如果那邊沒人放東西,你這邊取也取不到,也會發生死結!
如何解決這個問題呢?標準的做法是主動關閉管道,或者你知道你應該什麼時候關閉管道, 當然你結束程式管道自然也會關掉!針對上面的示範代碼,可以這樣寫:
i := 1for { str := <- ch println(str) if i >= 10{ close(ch) break } i++}
因為我們明確知道總共會輸出10個單詞,所以這裡簡單做了一個判斷,大於10就關閉管道退出for迴圈,就不會報錯了!下面是一個利用select從管道取資料的例子:
package mainimport ( "strconv" "fmt" "time")func main() { ch1 := make(chan int) ch2 := make(chan string) go pump1(ch1) go pump2(ch2) go suck(ch1, ch2) time.Sleep(time.Duration(time.Second*30))}func pump1(ch chan int) { for i := 0; ; i++ { ch <- i * 2 time.Sleep(time.Duration(time.Second)) }}func pump2(ch chan string) { for i := 0; ; i++ { ch <- strconv.Itoa(i+5) time.Sleep(time.Duration(time.Second)) }}func suck(ch1 chan int, ch2 chan string) { chRate := time.Tick(time.Duration(time.Second*5)) // 定時器 for { select { case v := <-ch1: fmt.Printf("Received on channel 1: %d\n", v) case v := <-ch2: fmt.Printf("Received on channel 2: %s\n", v) case <-chRate: fmt.Printf("Log log...\n") } }}
輸出結果如下:
Received on channel 1: 0Received on channel 2: 5Received on channel 2: 6Received on channel 1: 2Received on channel 1: 4Received on channel 2: 7Received on channel 1: 6Received on channel 2: 8Received on channel 2: 9Received on channel 1: 8Log log...Received on channel 2: 10Received on channel 1: 10Received on channel 1: 12Received on channel 2: 11Received on channel 2: 12Received on channel 1: 14
這個程式建立了2個管道一個傳輸int,一個傳輸string,同時啟動了3個協程,前2個協程非常簡單,就是每隔1s向管道輸出資料,第三個協程是不停的從管道取資料,
和之前的例子不一樣的地方是,pump1 和 pump2是2個不同的管道,通過select可以實現在不同管道之間切換,哪個管道有資料就從哪個管道裡面取資料,如果都沒資料就等著,
還有一個定時器功能可以每隔一段時間向管道輸出內容!
最後,值得一說的是,go 內建的web server效能非常強悍,主要就是因為使用了協程,對於每一個web請求,伺服器都會新開一個go協程去處理,
一個伺服器可以輕鬆同時開啟上萬個協程,好了,就說這麼多,感興趣的可以深入瞭解一下GoLang!