Go基礎(chǔ)語法(九)

Go語言并發(fā)

Go 是并發(fā)式語言,而不是并行式語言发乔。

并發(fā)是指立即處理多個任務(wù)的能力。

Go 編程語言原生支持并發(fā)雪猪。
Go 使用 Go 協(xié)程(Goroutine) 和信道(Channel)來處理并發(fā)栏尚。

Go 協(xié)程

Go 協(xié)程是與其他函數(shù)或方法一起并發(fā)運(yùn)行的函數(shù)或方法。
Go 協(xié)程可以看作是輕量級線程只恨。
與線程相比译仗,創(chuàng)建一個 Go 協(xié)程的成本很小。
因此在 Go 應(yīng)用中官觅,常常會看到有數(shù)以千計的 Go 協(xié)程并發(fā)地運(yùn)行纵菌。

優(yōu)勢

Go 協(xié)程相比于線程的優(yōu)勢:

  • Go 協(xié)程的成本極低,堆棧大小只有若干 kb休涤,且可以根據(jù)應(yīng)用的需求進(jìn)行增減咱圆。而線程必須指定堆棧的大小,其堆棧是固定不變的功氨。
  • Go 協(xié)程會復(fù)用數(shù)量更少的 OS 線程序苏。即使程序有數(shù)以千計的 Go 協(xié)程,也可能只有一個線程捷凄。如果該線程中的某一 Go 協(xié)程發(fā)生了阻塞(比如說等待用戶輸入)忱详,那么系統(tǒng)會再創(chuàng)建一個 OS 線程,并把其余 Go 協(xié)程都移動到這個新的 OS 線程跺涤。所有這一切都在運(yùn)行時進(jìn)行匈睁,程序員沒有直接面臨這些復(fù)雜的細(xì)節(jié),而是有一個簡潔的 API 來處理并發(fā)桶错。
  • Go 協(xié)程使用信道(Channel)來進(jìn)行通信航唆。信道用于防止多個協(xié)程訪問共享內(nèi)存時發(fā)生競態(tài)條件(Race Condition)。信道可以看作是 Go 協(xié)程之間通信的管道院刁。

啟動一個 Go 協(xié)程

調(diào)用函數(shù)或者方法時糯钙,在前面加上關(guān)鍵字 go,可以讓一個新的 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() 啟動了一個新的 Go 協(xié)程±璞龋現(xiàn)在 hello() 函數(shù)與 main() 函數(shù)會并發(fā)地執(zhí)行超营。
主函數(shù)會運(yùn)行在一個特有的 Go 協(xié)程上,它稱為 Go 主協(xié)程(Main Goroutine)阅虫。

執(zhí)行上邊代碼,你會發(fā)現(xiàn)程序只是打印出了main function 而未打印hello函數(shù)中的內(nèi)容不跟。這是因?yàn)椋?/p>

  • 啟動一個新的協(xié)程時颓帝,協(xié)程的調(diào)用會立即返回。與函數(shù)不同,程序控制不會去等待 Go 協(xié)程執(zhí)行完畢购城。在調(diào)用 Go 協(xié)程之后吕座,程序控制會立即返回到代碼的下一行,忽略該協(xié)程的任何返回值瘪板。

  • 如果希望運(yùn)行其他 Go 協(xié)程吴趴,Go 主協(xié)程必須繼續(xù)運(yùn)行著。如果 Go 主協(xié)程終止侮攀,則程序終止锣枝,于是其他 Go 協(xié)程也不會繼續(xù)運(yùn)行。

增加一行代碼延遲結(jié)束主協(xié)程:

time.Sleep(1 * time.Second)

這只是用于測試可以這樣寫兰英,事實(shí)上后邊我們會使用信道解決這個問題撇叁。

為了更好地理解 Go 協(xié)程,我們再編寫一個程序畦贸,啟動多個 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")
}

解釋代碼:

  1. 啟動了兩個 Go 協(xié)程。現(xiàn)在薄坏,這兩個協(xié)程并發(fā)地運(yùn)行趋厉。
  2. numbers 協(xié)程首先休眠 250 微秒,接著打印 1胶坠,然后再次休眠觅廓,打印 2,依此類推涵但,一直到打印 5 結(jié)束杈绸。
  3. alphabete 協(xié)程同樣打印從 a 到 e 的字母,并且每次有 400 微秒的休眠時間矮瘟。
  4. Go 主協(xié)程啟動了 numbers 和 alphabete 兩個 Go 協(xié)程瞳脓,休眠了 3000 微秒后終止程序。

來張圖更加清晰的看協(xié)程之間相互關(guān)系:


image.png

藍(lán)色的圖表示 numbers 協(xié)程澈侠,
褐紅色的圖表示 alphabets 協(xié)程劫侧,
綠色的圖表示 Go 主協(xié)程,
黑色的圖把以上三種協(xié)程合并了哨啃,表明程序是如何運(yùn)行的烧栋。

信道

信道:信道可以想像成 Go 協(xié)程之間通信的管道。如同管道中的水會從一端流到另一端拳球,通過使用信道审姓,數(shù)據(jù)也可以從一端發(fā)送,在另一端接收祝峻。

信道聲明:所有信道都關(guān)聯(lián)了一個類型魔吐。信道只能運(yùn)輸這種類型的數(shù)據(jù)扎筒,而運(yùn)輸其他類型的數(shù)據(jù)都是非法的。

chan T 表示 T類型的信道酬姆。
信道的零值為 nil

信道的零值沒有什么用嗜桌,應(yīng)該像對 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)
    }
}

簡短聲明通常也是一種定義信道的簡潔有效的方法:

a := make(chan int)
通過信道進(jìn)行發(fā)送和接收
data := <- a // 讀取信道 a  
a <- data // 寫入信道 a

在第一行骨宠,箭頭對于 a 來說是向外指的,因此我們讀取了信道 a 的值相满,并把該值存儲到變量 data层亿。

在第二行,箭頭指向了 a雳灵,因此我們在把數(shù)據(jù)寫入信道 a棕所。

發(fā)送與接收默認(rèn)是阻塞的

  • 當(dāng)把數(shù)據(jù)發(fā)送到信道時,程序控制會在發(fā)送數(shù)據(jù)的語句處發(fā)生阻塞悯辙,直到有其它 Go 協(xié)程從信道讀取到數(shù)據(jù)琳省,才會解除阻塞。
  • 當(dāng)讀取信道的數(shù)據(jù)時躲撰,如果沒有其它的協(xié)程把數(shù)據(jù)寫入到這個信道针贬,那么讀取過程就會一直阻塞著。

信道的這種特性能夠幫助 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")
}

解釋代碼:

  1. 創(chuàng)建了一個 bool 類型的信道 done,并把 done 作為參數(shù)傳遞給了 hello 協(xié)程
  2. <-done 這行代碼通過信道 done 接收數(shù)據(jù)谆棱,但并沒有使用數(shù)據(jù)或者把數(shù)據(jù)存儲到變量中快压。這完全是合法的。我們通過信道 done 接收數(shù)據(jù)垃瞧。這一行代碼發(fā)生了阻塞蔫劣,除非有協(xié)程向 done 寫入數(shù)據(jù),否則程序不會跳到下一行代碼个从。
  3. 現(xiàn)在我們的 Go 主協(xié)程發(fā)生了阻塞脉幢,等待信道 done 發(fā)送的數(shù)據(jù)。

寫個demo示例嗦锐,需求:定義一個整數(shù)嫌松,該程序會計算一個數(shù)中每一位的平方和與立方和,然后把平方和與立方和相加并打印出來奕污。

構(gòu)建程序:

  • 一個單獨(dú)的 Go 協(xié)程計算平方和
  • 一個協(xié)程計算立方和萎羔,
  • 在 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é)程給一個信道發(fā)送數(shù)據(jù)時菊值,照理說會有其他 Go 協(xié)程來接收數(shù)據(jù)外驱。如果沒有的話育灸,程序就會在運(yùn)行時觸發(fā) panic腻窒,形成死鎖昵宇。同樣的反之亦然。

package main

func main() {  
    ch := make(chan int)
    ch <- 5
}

這段代碼就會觸發(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) 編譯器會報錯。

信道轉(zhuǎn)換(Channel Conversion)

把一個雙向信道轉(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)換為一個唯送信道。于是該信道在 sendData 協(xié)程里是一個唯送信道癌蓖,而在 Go 主協(xié)程里是一個雙向信道瞬哼。該程序最終打印輸出 10。

關(guān)閉信道和使用 for range 遍歷信道

數(shù)據(jù)發(fā)送方可以關(guān)閉信道租副,通知接收方這個信道不再有數(shù)據(jù)發(fā)送過來坐慰。

當(dāng)從信道接收數(shù)據(jù)時,接收方可以多用一個變量來檢查信道是否已經(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é)程會從 0 到 9 寫入信道 chn1结胀,然后關(guān)閉該信道。主函數(shù)有一個無限的 for 循環(huán)(第 16 行)责循,使用變量 ok(第 18 行)檢查信道是否已經(jīng)關(guān)閉糟港。如果 ok 等于 false,說明信道已經(jīng)關(guān)閉沼死,于是退出 for 循環(huán)着逐。如果 ok 等于 true,會打印出接收到的值和 ok 的值意蛀。

for range 循環(huán)用于在一個信道關(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)
}

如果本文對您有幫助县钥,記得點(diǎn)個小贊~~~
關(guān)注作者持續(xù)更新~~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末秀姐,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子若贮,更是在濱河造成了極大的恐慌省有,老刑警劉巖痒留,帶你破解...
    沈念sama閱讀 212,185評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異蠢沿,居然都是意外死亡伸头,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,445評論 3 385
  • 文/潘曉璐 我一進(jìn)店門舷蟀,熙熙樓的掌柜王于貴愁眉苦臉地迎上來恤磷,“玉大人,你說我怎么就攤上這事野宜∩ú剑” “怎么了?”我有些...
    開封第一講書人閱讀 157,684評論 0 348
  • 文/不壞的土叔 我叫張陵匈子,是天一觀的道長河胎。 經(jīng)常有香客問我,道長虎敦,這世上最難降的妖魔是什么游岳? 我笑而不...
    開封第一講書人閱讀 56,564評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮原茅,結(jié)果婚禮上吭历,老公的妹妹穿的比我還像新娘。我一直安慰自己擂橘,他們只是感情好晌区,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,681評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著通贞,像睡著了一般朗若。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上昌罩,一...
    開封第一講書人閱讀 49,874評論 1 290
  • 那天哭懈,我揣著相機(jī)與錄音,去河邊找鬼茎用。 笑死遣总,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的轨功。 我是一名探鬼主播旭斥,決...
    沈念sama閱讀 39,025評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼古涧!你這毒婦竟也來了垂券?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,761評論 0 268
  • 序言:老撾萬榮一對情侶失蹤羡滑,失蹤者是張志新(化名)和其女友劉穎菇爪,沒想到半個月后算芯,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,217評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡凳宙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,545評論 2 327
  • 正文 我和宋清朗相戀三年熙揍,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片近速。...
    茶點(diǎn)故事閱讀 38,694評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡诈嘿,死狀恐怖堪旧,靈堂內(nèi)的尸體忽然破棺而出削葱,到底是詐尸還是另有隱情,我是刑警寧澤淳梦,帶...
    沈念sama閱讀 34,351評論 4 332
  • 正文 年R本政府宣布析砸,位于F島的核電站,受9級特大地震影響爆袍,放射性物質(zhì)發(fā)生泄漏首繁。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,988評論 3 315
  • 文/蒙蒙 一陨囊、第九天 我趴在偏房一處隱蔽的房頂上張望弦疮。 院中可真熱鬧,春花似錦蜘醋、人聲如沸胁塞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,778評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽啸罢。三九已至,卻和暖如春胎食,著一層夾襖步出監(jiān)牢的瞬間扰才,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,007評論 1 266
  • 我被黑心中介騙來泰國打工厕怜, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留衩匣,地道東北人。 一個月前我還...
    沈念sama閱讀 46,427評論 2 360
  • 正文 我出身青樓粥航,卻偏偏與公主長得像琅捏,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子躁锡,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,580評論 2 349

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理午绳,服務(wù)發(fā)現(xiàn),斷路器映之,智...
    卡卡羅2017閱讀 134,633評論 18 139
  • 輕量級線程:協(xié)程 在常用的并發(fā)模型中,多進(jìn)程赎败、多線程秕衙、分布式是最普遍的,不過近些年來逐漸有一些語言以first-c...
    Tenderness4閱讀 6,357評論 2 10
  • 原文鏈接:https://github.com/EasyKotlin 在常用的并發(fā)模型中僵刮,多進(jìn)程据忘、多線程、分布式是...
    JackChen1024閱讀 10,715評論 3 23
  • Go-ethereum 源碼解析之 miner/worker.go (下) Appendix D. 詳細(xì)批注 1....
    furnace閱讀 1,921評論 0 0
  • 矛盾的心理是我們的天性 by 楊海軒 我想要拿高分我要考上清華大學(xué)我的工資是我的小伙伴中最高的我拿著這么高的工資太...
    宵軒閱讀 288評論 0 0