在前面的教程里复斥,我們探討了并發(fā)悬秉,以及并發(fā)與并行的區(qū)別澄步。本教程則會介紹在 Go 語言里,如何使用 Go 協程(Goroutine)來實現并發(fā)和泌。
Go 協程是什么村缸?
Go 協程是與其他函數或方法一起并發(fā)運行的函數或方法。Go 協程可以看作是輕量級線程武氓。與線程相比梯皿,創(chuàng)建一個 Go 協程的成本很小仇箱。因此在 Go 應用中,常常會看到有數以千計的 Go 協程并發(fā)地運行东羹。
Go 協程相比于線程的優(yōu)勢
相比線程而言剂桥,Go 協程的成本極低。堆棧大小只有若干 kb属提,并且可以根據應用的需求進行增減权逗。而線程必須指定堆棧的大小,其堆棧是固定不變的冤议。
Go 協程會復用(Multiplex)數量更少的 OS 線程斟薇。即使程序有數以千計的 Go 協程,也可能只有一個線程恕酸。如果該線程中的某一 Go 協程發(fā)生了阻塞(比如說等待用戶輸入)堪滨,那么系統會再創(chuàng)建一個 OS 線程,并把其余 Go 協程都移動到這個新的 OS 線程蕊温。所有這一切都在運行時進行袱箱,作為程序員,我們沒有直接面臨這些復雜的細節(jié)寿弱,而是有一個簡潔的 API 來處理并發(fā)犯眠。
Go 協程使用信道(Channel)來進行通信。信道用于防止多個協程訪問共享內存時發(fā)生競態(tài)條件(Race Condition)症革。信道可以看作是 Go 協程之間通信的管道筐咧。我們會在下一教程詳細討論信道。
如何啟動一個 Go 協程噪矛?
調用函數或者方法時量蕊,在前面加上關鍵字 go,可以讓一個新的 Go 協程并發(fā)地運行艇挨。
讓我們創(chuàng)建一個 Go 協程吧残炮。
package main
import (
"fmt"
)
func hello() {
fmt.Println("Hello world goroutine")
}
func main() {
go hello()
fmt.Println("main function")
}
在第 11 行,go hello() 啟動了一個新的 Go 協程∷醣酰現在 hello() 函數與 main() 函數會并發(fā)地執(zhí)行势就。主函數會運行在一個特有的 Go 協程上,它稱為 Go 主協程(Main Goroutine)脉漏。
運行一下程序苞冯,你會很驚訝!
該程序只會輸出文本 main function侧巨。我們啟動的 Go 協程究竟出現了什么問題舅锄?要理解這一切,我們需要理解兩個 Go 協程的主要性質司忱。
啟動一個新的協程時皇忿,協程的調用會立即返回畴蹭。與函數不同,程序控制不會去等待 Go 協程執(zhí)行完畢鳍烁。在調用 Go 協程之后叨襟,程序控制會立即返回到代碼的下一行,忽略該協程的任何返回值老翘。
如果希望運行其他 Go 協程芹啥,Go 主協程必須繼續(xù)運行著。如果 Go 主協程終止铺峭,則程序終止墓怀,于是其他 Go 協程也不會繼續(xù)運行。
現在你應該能夠理解卫键,為何我們的 Go 協程沒有運行了吧傀履。在第 11 行調用了 go hello() 之后,程序控制沒有等待 hello 協程結束莉炉,立即返回到了代碼下一行钓账,打印 main function。接著由于沒有其他可執(zhí)行的代碼絮宁,Go 主協程終止梆暮,于是 hello 協程就沒有機會運行了。
我們現在修復這個問題绍昂。
package main
import (
"fmt"
"time"
)
func hello() {
fmt.Println("Hello world goroutine")
}
func main() {
go hello()
time.Sleep(1 * time.Second)
fmt.Println("main function")
}
在上面程序的第 13 行啦粹,我們調用了 time 包里的函數 Sleep
,該函數會休眠執(zhí)行它的 Go 協程窘游。在這里唠椭,我們使 Go 主協程休眠了 1 秒。因此在主協程終止之前忍饰,調用 go hello()
就有足夠的時間來執(zhí)行了贪嫂。該程序首先打印 Hello world goroutine
,等待 1 秒鐘之后艾蓝,接著打印 main function
力崇。
在 Go 主協程中使用休眠,以便等待其他協程執(zhí)行完畢赢织,這種方法只是用于理解 Go 協程如何工作的技巧餐曹。信道可用于在其他協程結束執(zhí)行之前,阻塞 Go 主協程敌厘。我們會在下一教程中討論信道。
啟動多個 Go 協程
為了更好地理解 Go 協程朽合,我們再編寫一個程序俱两,啟動多個 Go 協程饱狂。
package main
import (
"fmt"
"time"
)
func numbers() {
for i := 1; i <= 5; i++ {
time.Sleep(250 * time.Millisecond)
fmt.Printf("%d ", i)
}
}
func alphabets() {
for i := 'a'; i <= 'e'; i++ {
time.Sleep(400 * time.Millisecond)
fmt.Printf("%c ", i)
}
}
func main() {
go numbers()
go alphabets()
time.Sleep(3000 * time.Millisecond)
fmt.Println("main terminated")
}
在上面程序中的第 21 行和第 22 行,啟動了兩個 Go 協程∠懿剩現在休讳,這兩個協程并發(fā)地運行。numbers 協程首先休眠 250 微秒尿孔,接著打印 1俊柔,然后再次休眠,打印 2活合,依此類推雏婶,一直到打印 5 結束。alphabete 協程同樣打印從 a 到 e 的字母白指,并且每次有 400 微秒的休眠時間留晚。 Go 主協程啟動了 numbers 和 alphabete 兩個 Go 協程,休眠了 3000 微秒后終止程序告嘲。
該程序會輸出:
1 a 2 3 b 4 c 5 d e main terminated
程序的運作如下圖所示错维。為了更好地觀看圖片,請在新標簽頁中打開橄唬。
第一張藍色的圖表示 numbers 協程赋焕,第二張褐紅色的圖表示 alphabets 協程,第三張綠色的圖表示 Go 主協程仰楚,而最后一張黑色的圖把以上三種協程合并了隆判,表明程序是如何運行的。在每個方框頂部缸血,諸如 0 ms 和 250 ms 這樣的字符串表示時間(以微秒為單位)蜜氨。在每個方框的底部,1捎泻、2飒炎、3 等表示輸出。藍色方框表示:250 ms 打印出 1笆豁,500 ms 打印出 2郎汪,依此類推。最后黑色方框的底部的值會是 1 a 2 3 b 4 c 5 d e main terminated闯狱,這同樣也是整個程序的輸出煞赢。以上圖片非常直觀,你可以用它來理解程序是如何運作的哄孤。
Go 協程的介紹到此結束照筑。祝你愉快。