Go語言并發(fā)
Go 是并發(fā)式語言,而不是并行式語言。
并發(fā)是指立即處理多個(gè)任務(wù)的能力。
Go 編程語言原生支持并發(fā)。
Go 使用 Go 協(xié)程(Goroutine) 和信道(Channel)來處理并發(fā)。
Go 協(xié)程
Go 協(xié)程是與其他函數(shù)或方法一起并發(fā)運(yùn)行的函數(shù)或方法。
Go 協(xié)程可以看作是輕量級(jí)線程。
與線程相比,創(chuàng)建一個(gè) Go 協(xié)程的成本很小。
因此在 Go 應(yīng)用中,常常會(huì)看到有數(shù)以千計(jì)的 Go 協(xié)程并發(fā)地運(yùn)行。
優(yōu)勢(shì)
Go 協(xié)程相比于線程的優(yōu)勢(shì):
- Go 協(xié)程的成本極低,堆棧大小只有若干 kb,且可以根據(jù)應(yīng)用的需求進(jìn)行增減。而線程必須指定堆棧的大小,其堆棧是固定不變的。
- Go 協(xié)程會(huì)復(fù)用數(shù)量更少的 OS 線程。即使程序有數(shù)以千計(jì)的 Go 協(xié)程,也可能只有一個(gè)線程。如果該線程中的某一 Go 協(xié)程發(fā)生了阻塞(比如說等待用戶輸入),那么系統(tǒng)會(huì)再創(chuàng)建一個(gè) OS 線程,并把其余 Go 協(xié)程都移動(dòng)到這個(gè)新的 OS 線程。所有這一切都在運(yùn)行時(shí)進(jìn)行,程序員沒有直接面臨這些復(fù)雜的細(xì)節(jié),而是有一個(gè)簡(jiǎn)潔的 API 來處理并發(fā)。
- Go 協(xié)程使用信道(Channel)來進(jìn)行通信。信道用于防止多個(gè)協(xié)程訪問共享內(nèi)存時(shí)發(fā)生競(jìng)態(tài)條件(Race Condition)。信道可以看作是 Go 協(xié)程之間通信的管道。
啟動(dòng)一個(gè) Go 協(xié)程
調(diào)用函數(shù)或者方法時(shí),在前面加上關(guān)鍵字 go,可以讓一個(gè)新的 Go 協(xié)程并發(fā)地運(yùn)行
package main
import (
"fmt"
)
func hello() {
fmt.Println("Hello world goroutine")
}
func main() {
go hello()
fmt.Println("main function")
}
解釋代碼:go hello() 啟動(dòng)了一個(gè)新的 Go 協(xié)程?,F(xiàn)在 hello() 函數(shù)與 main() 函數(shù)會(huì)并發(fā)地執(zhí)行。
主函數(shù)會(huì)運(yùn)行在一個(gè)特有的 Go 協(xié)程上,它稱為 Go 主協(xié)程(Main Goroutine)。
執(zhí)行上邊代碼,你會(huì)發(fā)現(xiàn)程序只是打印出了main function 而未打印hello函數(shù)中的內(nèi)容。這是因?yàn)椋?/p>
啟動(dòng)一個(gè)新的協(xié)程時(shí),協(xié)程的調(diào)用會(huì)立即返回。與函數(shù)不同,程序控制不會(huì)去等待 Go 協(xié)程執(zhí)行完畢。在調(diào)用 Go 協(xié)程之后,程序控制會(huì)立即返回到代碼的下一行,忽略該協(xié)程的任何返回值。
如果希望運(yùn)行其他 Go 協(xié)程,Go 主協(xié)程必須繼續(xù)運(yùn)行著。如果 Go 主協(xié)程終止,則程序終止,于是其他 Go 協(xié)程也不會(huì)繼續(xù)運(yùn)行。
增加一行代碼延遲結(jié)束主協(xié)程:
time.Sleep(1 * time.Second)
這只是用于測(cè)試可以這樣寫,事實(shí)上后邊我們會(huì)使用信道解決這個(gè)問題。
為了更好地理解 Go 協(xié)程,我們?cè)倬帉懸粋€(gè)程序,啟動(dòng)多個(gè) Go 協(xié)程。
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")
}
解釋代碼:
- 啟動(dòng)了兩個(gè) Go 協(xié)程?,F(xiàn)在,這兩個(gè)協(xié)程并發(fā)地運(yùn)行。
- numbers 協(xié)程首先休眠 250 微秒,接著打印 1,然后再次休眠,打印 2,依此類推,一直到打印 5 結(jié)束。
- alphabete 協(xié)程同樣打印從 a 到 e 的字母,并且每次有 400 微秒的休眠時(shí)間。
- Go 主協(xié)程啟動(dòng)了 numbers 和 alphabete 兩個(gè) Go 協(xié)程,休眠了 3000 微秒后終止程序。
來張圖更加清晰的看協(xié)程之間相互關(guān)系:

藍(lán)色的圖表示 numbers 協(xié)程,
褐紅色的圖表示 alphabets 協(xié)程,
綠色的圖表示 Go 主協(xié)程,
黑色的圖把以上三種協(xié)程合并了,表明程序是如何運(yùn)行的。
信道
信道:信道可以想像成 Go 協(xié)程之間通信的管道。如同管道中的水會(huì)從一端流到另一端,通過使用信道,數(shù)據(jù)也可以從一端發(fā)送,在另一端接收。
信道聲明:所有信道都關(guān)聯(lián)了一個(gè)類型。信道只能運(yùn)輸這種類型的數(shù)據(jù),而運(yùn)輸其他類型的數(shù)據(jù)都是非法的。
chan T 表示 T類型的信道。
信道的零值為 nil
信道的零值沒有什么用,應(yīng)該像對(duì) map 和切片所做的那樣,用 make 來定義信道。
package main
import "fmt"
func main() {
var a chan int
if a == nil {
fmt.Println("channel a is nil, going to define it")
a = make(chan int)
fmt.Printf("Type of a is %T", a)
}
}
簡(jiǎn)短聲明通常也是一種定義信道的簡(jiǎn)潔有效的方法:
a := make(chan int)
通過信道進(jìn)行發(fā)送和接收
data := <- a // 讀取信道 a
a <- data // 寫入信道 a
在第一行,箭頭對(duì)于 a 來說是向外指的,因此我們讀取了信道 a 的值,并把該值存儲(chǔ)到變量 data。
在第二行,箭頭指向了 a,因此我們?cè)诎褦?shù)據(jù)寫入信道 a。
發(fā)送與接收默認(rèn)是阻塞的
- 當(dāng)把數(shù)據(jù)發(fā)送到信道時(shí),程序控制會(huì)在發(fā)送數(shù)據(jù)的語句處發(fā)生阻塞,直到有其它 Go 協(xié)程從信道讀取到數(shù)據(jù),才會(huì)解除阻塞。
- 當(dāng)讀取信道的數(shù)據(jù)時(shí),如果沒有其它的協(xié)程把數(shù)據(jù)寫入到這個(gè)信道,那么讀取過程就會(huì)一直阻塞著。
信道的這種特性能夠幫助 Go 協(xié)程之間進(jìn)行高效的通信,不需要用到其他編程語言常見的顯式鎖或條件變量。
代碼示例:
package main
import (
"fmt"
)
func hello(done chan bool) {
fmt.Println("Hello world goroutine")
done <- true
}
func main() {
done := make(chan bool)
go hello(done)
<-done
fmt.Println("main function")
}
解釋代碼:
- 創(chuàng)建了一個(gè) bool 類型的信道 done,并把 done 作為參數(shù)傳遞給了 hello 協(xié)程
- <-done 這行代碼通過信道 done 接收數(shù)據(jù),但并沒有使用數(shù)據(jù)或者把數(shù)據(jù)存儲(chǔ)到變量中。這完全是合法的。我們通過信道 done 接收數(shù)據(jù)。這一行代碼發(fā)生了阻塞,除非有協(xié)程向 done 寫入數(shù)據(jù),否則程序不會(huì)跳到下一行代碼。
- 現(xiàn)在我們的 Go 主協(xié)程發(fā)生了阻塞,等待信道 done 發(fā)送的數(shù)據(jù)。
寫個(gè)demo示例,需求:定義一個(gè)整數(shù),該程序會(huì)計(jì)算一個(gè)數(shù)中每一位的平方和與立方和,然后把平方和與立方和相加并打印出來。
構(gòu)建程序:
- 一個(gè)單獨(dú)的 Go 協(xié)程計(jì)算平方和
- 一個(gè)協(xié)程計(jì)算立方和,
- 在 Go 主協(xié)程把平方和與立方和相加。
package main
import (
"fmt"
)
func calcSquares(number int, squareop chan int) {
sum := 0
for number != 0 {
digit := number % 10
sum += digit * digit
number /= 10
}
squareop <- sum
}
func calcCubes(number int, cubeop chan int) {
sum := 0
for number != 0 {
digit := number % 10
sum += digit * digit * digit
number /= 10
}
cubeop <- sum
}
func main() {
number := 589
sqrch := make(chan int)
cubech := make(chan int)
go calcSquares(number, sqrch)
go calcCubes(number, cubech)
squares, cubes := <-sqrch, <-cubech
fmt.Println("Final output", squares + cubes)
}
死鎖
當(dāng) Go 協(xié)程給一個(gè)信道發(fā)送數(shù)據(jù)時(shí),照理說會(huì)有其他 Go 協(xié)程來接收數(shù)據(jù)。如果沒有的話,程序就會(huì)在運(yùn)行時(shí)觸發(fā) panic,形成死鎖。同樣的反之亦然。
package main
func main() {
ch := make(chan int)
ch <- 5
}
這段代碼就會(huì)觸發(fā) panic :
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
/tmp/sandbox249677995/main.go:6 +0x80
單向信道
之前介紹的信道都是雙向信道,即通過信道既能發(fā)送數(shù)據(jù),又能接收數(shù)據(jù)。
其實(shí)也可以創(chuàng)建單向信道,這種信道只能發(fā)送或者接收數(shù)據(jù)。
代碼:
package main
import "fmt"
func sendData(sendch chan<- int) {
sendch <- 10
}
func main() {
sendch := make(chan<- int)
go sendData(sendch)
fmt.Println(<-sendch)
}
創(chuàng)建了唯送(Send Only)信道 sendch。chan<- int 定義了唯送信道,因?yàn)榧^指向了 chan。 fmt.Println(<-sendch) 編譯器會(huì)報(bào)錯(cuò)。
信道轉(zhuǎn)換(Channel Conversion)
把一個(gè)雙向信道轉(zhuǎn)換成唯送信道或者唯收(Receive Only)信道都是行得通的,但是反過來就不行。
package main
import "fmt"
func sendData(sendch chan<- int) {
sendch <- 10
}
func main() {
cha1 := make(chan int)
go sendData(cha1)
fmt.Println(<-cha1)
}
解釋代碼:
函數(shù) sendData 里的參數(shù) sendch chan<- int把 cha1 轉(zhuǎn)換為一個(gè)唯送信道。于是該信道在 sendData 協(xié)程里是一個(gè)唯送信道,而在 Go 主協(xié)程里是一個(gè)雙向信道。該程序最終打印輸出 10。
關(guān)閉信道和使用 for range 遍歷信道
數(shù)據(jù)發(fā)送方可以關(guān)閉信道,通知接收方這個(gè)信道不再有數(shù)據(jù)發(fā)送過來。
當(dāng)從信道接收數(shù)據(jù)時(shí),接收方可以多用一個(gè)變量來檢查信道是否已經(jīng)關(guān)閉。
v, ok := <- ch
package main
import (
"fmt"
)
func producer(chnl chan int) {
for i := 0; i < 10; i++ {
chnl <- i
}
close(chnl)
}
func main() {
ch := make(chan int)
go producer(ch)
for {
v, ok := <-ch
if ok == false {
break
}
fmt.Println("Received ", v, ok)
}
}
producer 協(xié)程會(huì)從 0 到 9 寫入信道 chn1,然后關(guān)閉該信道。主函數(shù)有一個(gè)無限的 for 循環(huán)(第 16 行),使用變量 ok(第 18 行)檢查信道是否已經(jīng)關(guān)閉。如果 ok 等于 false,說明信道已經(jīng)關(guān)閉,于是退出 for 循環(huán)。如果 ok 等于 true,會(huì)打印出接收到的值和 ok 的值。
for range 循環(huán)用于在一個(gè)信道關(guān)閉之前,從信道接收數(shù)據(jù)。
package main
import (
"fmt"
)
func producer(chnl chan int) {
for i := 0; i < 10; i++ {
chnl <- i
}
close(chnl)
}
func main() {
ch := make(chan int)
go producer(ch)
for v := range ch {
fmt.Println("Received ",v)
}
}
package main
import (
"fmt"
)
func digits(number int, dchnl chan int) {
for number != 0 {
digit := number % 10
dchnl <- digit
number /= 10
}
close(dchnl)
}
func calcSquares(number int, squareop chan int) {
sum := 0
dch := make(chan int)
go digits(number, dch)
for digit := range dch {
sum += digit * digit
}
squareop <- sum
}
func calcCubes(number int, cubeop chan int) {
sum := 0
dch := make(chan int)
go digits(number, dch)
for digit := range dch {
sum += digit * digit * digit
}
cubeop <- sum
}
func main() {
number := 589
sqrch := make(chan int)
cubech := make(chan int)
go calcSquares(number, sqrch)
go calcCubes(number, cubech)
squares, cubes := <-sqrch, <-cubech
fmt.Println("Final output", squares+cubes)
}
如果本文對(duì)您有幫助,記得點(diǎn)個(gè)小贊~~~
關(guān)注作者持續(xù)更新~~~