Golang(十四) 并發(fā)性Concurrency

并發(fā)性Concurrency

1.1 什么是并發(fā)

Go是并發(fā)語(yǔ)言管引,而不是并行語(yǔ)言士败。在討論如何在Go中進(jìn)行并發(fā)處理之前,我們首先必須了解什么是并發(fā)褥伴,以及它與并行性有什么不同谅将。(Go is a concurrent language and not a parallel one. )

并發(fā)性Concurrency是同時(shí)處理許多事情的能力。

舉個(gè)例子重慢,假設(shè)一個(gè)人在晨跑戏自。在晨跑時(shí),他的鞋帶松了∩嗣現(xiàn)在這個(gè)人停止跑步擅笔,系鞋帶,然后又開始跑步屯援。這是一個(gè)典型的并發(fā)性示例猛们。這個(gè)人能夠同時(shí)處理跑步和系鞋帶,這是一個(gè)人能夠同時(shí)處理很多事情狞洋。

什么是并行性parallelism弯淘,它與并發(fā)concurrency有什么不同? 并行就是同時(shí)做很多事情。這聽起來(lái)可能與并發(fā)類似吉懊,但實(shí)際上是不同的庐橙。

讓我們用同樣的慢跑例子更好地理解它。在這種情況下借嗽,我們假設(shè)這個(gè)人正在慢跑态鳖,并且使用它的手機(jī)聽音樂。在這種情況下恶导,一個(gè)人一邊慢跑一邊聽音樂浆竭,那就是他同時(shí)在做很多事情。這就是所謂的并行性(parallelism)惨寿。

并發(fā)性和并行性——一種技術(shù)上的觀點(diǎn)邦泄。 設(shè)我們正在編寫一個(gè)web瀏覽器。web瀏覽器有各種組件裂垦。其中兩個(gè)是web頁(yè)面呈現(xiàn)區(qū)域和下載文件從internet下載的下載器顺囊。假設(shè)我們以這樣的方式構(gòu)建了瀏覽器的代碼,這樣每個(gè)組件都可以獨(dú)立地執(zhí)行(這是在Java和Go中使用線程來(lái)完成的蕉拢,我們可以在稍后使用Goroutines來(lái)實(shí)現(xiàn)這一點(diǎn))特碳。當(dāng)這個(gè)瀏覽器運(yùn)行在單個(gè)核處理器中時(shí)诚亚,處理器將在瀏覽器的兩個(gè)組件之間進(jìn)行上下文切換。它可能會(huì)下載一個(gè)文件一段時(shí)間测萎,然后它可能會(huì)切換到呈現(xiàn)用戶請(qǐng)求的網(wǎng)頁(yè)的html。這就是所謂的并發(fā)性届巩。并發(fā)進(jìn)程從不同的時(shí)間點(diǎn)開始硅瞧,它們的執(zhí)行周期重疊。在這種情況下恕汇,下載和呈現(xiàn)從不同的時(shí)間點(diǎn)開始腕唧,它們的執(zhí)行重疊。假設(shè)同一瀏覽器運(yùn)行在多核處理器上瘾英。在這種情況下枣接,文件下載組件和HTML呈現(xiàn)組件可能同時(shí)在不同的內(nèi)核中運(yùn)行。這就是所謂的并行性缺谴。

并行性Parallelism不會(huì)總是導(dǎo)致更快的執(zhí)行時(shí)間但惶。這是因?yàn)椴⑿羞\(yùn)行的組件可能需要相互通信。例如湿蛔,在我們的瀏覽器中膀曾,當(dāng)文件下載完成時(shí),應(yīng)該將其傳遞給用戶阳啥,比如使用彈出窗口添谊。這種通信發(fā)生在負(fù)責(zé)下載的組件和負(fù)責(zé)呈現(xiàn)用戶界面的組件之間。這種通信開銷在并發(fā)concurrent 系統(tǒng)中很低察迟。當(dāng)組件在多個(gè)內(nèi)核中并行concurrent 運(yùn)行時(shí)斩狱,這種通信開銷很高。因此扎瓶,并行程序并不總是導(dǎo)致更快的執(zhí)行時(shí)間!

1.2 Goroutines

1.2.1 什么是Goroutines

go中使用Goroutines來(lái)實(shí)現(xiàn)并發(fā)concurrently爆阶。Goroutines是與其他函數(shù)或方法同時(shí)運(yùn)行的函數(shù)或方法。Goroutines可以被認(rèn)為是輕量級(jí)的線程耳贬。與線程相比规揪,創(chuàng)建Goroutine的成本很小。因此乍赫,Go應(yīng)用程序可以并發(fā)運(yùn)行數(shù)千個(gè)Goroutines瓣蛀。

Goroutines在線程上的優(yōu)勢(shì)。

  1. 與線程相比雷厂,Goroutines非常便宜惋增。它們只是堆棧大小的幾個(gè)kb,堆椄啮辏可以根據(jù)應(yīng)用程序的需要增長(zhǎng)和收縮诈皿,而在線程的情況下林束,堆棧大小必須指定并且是固定的

  2. Goroutines被多路復(fù)用到較少的OS線程。在一個(gè)程序中可能只有一個(gè)線程與數(shù)千個(gè)Goroutines稽亏。如果線程中的任何Goroutine都表示等待用戶輸入壶冒,則會(huì)創(chuàng)建另一個(gè)OS線程,剩下的Goroutines被轉(zhuǎn)移到新的OS線程截歉。所有這些都由運(yùn)行時(shí)進(jìn)行處理胖腾,我們作為程序員從這些復(fù)雜的細(xì)節(jié)中抽象出來(lái),并得到了一個(gè)與并發(fā)工作相關(guān)的干凈的API瘪松。

  3. 當(dāng)使用Goroutines訪問共享內(nèi)存時(shí)咸作,通過設(shè)計(jì)的通道可以防止競(jìng)態(tài)條件發(fā)生。通道可以被認(rèn)為是Goroutines通信的管道宵睦。

1.2.2 如何使用Goroutines

在函數(shù)或方法調(diào)用前面加上關(guān)鍵字go记罚,您將會(huì)同時(shí)運(yùn)行一個(gè)新的Goroutine。

實(shí)例代碼:

package main

import (
    "fmt"
)

func hello() {
    fmt.Println("Hello world goroutine")
}
func main() {
    go hello()
    fmt.Println("main function")
}

運(yùn)行結(jié)果:可能會(huì)只輸出main function壳嚎。

我們開始的Goroutine怎么樣了?我們需要了解Goroutine的規(guī)則

  1. 當(dāng)新的Goroutine開始時(shí)桐智,Goroutine調(diào)用立即返回。與函數(shù)不同烟馅,go不等待Goroutine執(zhí)行結(jié)束酵使。當(dāng)Goroutine調(diào)用,并且Goroutine的任何返回值被忽略之后焙糟,go立即執(zhí)行到下一行代碼口渔。

  2. main的Goroutine應(yīng)該為其他的Goroutines執(zhí)行。如果main的Goroutine終止了穿撮,程序?qū)⒈唤K止缺脉,而其他Goroutine將不會(huì)運(yùn)行。

修改以上代碼:

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")
}

運(yùn)行結(jié)果:

Hello world goroutine
main function

在上面的程序中悦穿,我們已經(jīng)調(diào)用了時(shí)間包的Sleep方法攻礼,它會(huì)在執(zhí)行過程中睡覺。在這種情況下栗柒,main的goroutine被用來(lái)睡覺1秒〗赴纾現(xiàn)在調(diào)用go hello()有足夠的時(shí)間在main Goroutine終止之前執(zhí)行。這個(gè)程序首先打印Hello world goroutine瞬沦,等待1秒太伊,然后打印main函數(shù)。

1.2.3 啟動(dòng)多個(gè)Goroutines

示例代碼:

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")
}

運(yùn)行結(jié)果:

1 a 2 3 b 4 c 5 d e main terminated

時(shí)間軸分析:

1.3通道channels

通道可以被認(rèn)為是Goroutines通信的管道逛钻。類似于管道中的水從一端到另一端的流動(dòng)僚焦,數(shù)據(jù)可以從一端發(fā)送到另一端,通過通道接收曙痘。

1.3.1 聲明通道

每個(gè)通道都有與其相關(guān)的類型芳悲。該類型是通道允許傳輸?shù)臄?shù)據(jù)類型立肘。(通道的零值為nil。nil通道沒有任何用處名扛,因此通道必須使用類似于地圖和切片的方法來(lái)定義谅年。)

示例代碼:

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)
    }
}

運(yùn)行結(jié)果:

channel a is nil, going to define it
Type of a is chan int

也可以簡(jiǎn)短的聲明:

a := make(chan int)

1.3.2 發(fā)送和接收

發(fā)送和接收的語(yǔ)法:

data := <- a // read from channel a
a <- data // write to channel a

在通道上箭頭的方向指定數(shù)據(jù)是發(fā)送還是接收。

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

一個(gè)通道發(fā)送和接收數(shù)據(jù)耸峭,默認(rèn)是阻塞的劳闹。當(dāng)一個(gè)數(shù)據(jù)被發(fā)送到通道時(shí)本涕,在發(fā)送語(yǔ)句中被阻塞,直到另一個(gè)Goroutine從該通道讀取數(shù)據(jù)菩颖。類似地晦闰,當(dāng)從通道讀取數(shù)據(jù)時(shí)跪妥,讀取被阻塞,直到一個(gè)Goroutine將數(shù)據(jù)寫入該通道。

這些通道的特性是幫助Goroutines有效地進(jìn)行通信憾赁,而無(wú)需像使用其他編程語(yǔ)言中非常常見的顯式鎖或條件變量仰挣。

示例代碼:

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 // 接收數(shù)據(jù)颓芭,阻塞式
    fmt.Println("main function")
}

運(yùn)行結(jié)果:

Hello world goroutine
main function

在上面的程序中,我們?cè)诘谝恍兄袆?chuàng)建了一個(gè)done bool通道束世。把它作為參數(shù)傳遞給hello Goroutine。第14行我們正在接收已完成頻道的數(shù)據(jù)。這一行代碼是阻塞的缨该,這意味著在某些Goroutine將數(shù)據(jù)寫入到已完成的通道之前,程序?qū)⒉粫?huì)執(zhí)行到下一行代碼。因此询一,這就消除了對(duì)時(shí)間的需求缩功。睡眠在原來(lái)的程序中虑稼,以防止主要的Goroutine退出琳钉。

代碼<-done接收來(lái)自done Goroutine的數(shù)據(jù),但不使用或存儲(chǔ)任何變量中的數(shù)據(jù)蛛倦。這是完全合法的歌懒。

現(xiàn)在,我們的main Goroutine阻塞等待已完成通道的數(shù)據(jù)溯壶。hello Goroutine接收這個(gè)通道作為參數(shù)及皂,打印hello world Goroutine,然后寫入done通道且改。當(dāng)此寫入完成時(shí)验烧,main的Goroutine接收來(lái)自已完成通道的數(shù)據(jù),它是未阻塞的又跛,然后輸出文本主函數(shù)碍拆。

讓我們通過在hello Goroutine中引入睡眠來(lái)修改這個(gè)程序,以更好地理解這個(gè)阻塞的概念效扫。

package main

import (
    "fmt"
    "time"
)

func hello(done chan bool) {
    fmt.Println("hello go routine is going to sleep")
    time.Sleep(4 * time.Second)
    fmt.Println("hello go routine awake and going to write to done")
    done <- true
}
func main() {
    done := make(chan bool)
    fmt.Println("Main going to call hello go goroutine")
    go hello(done)
    <-done
    fmt.Println("Main received data")
}

再一個(gè)例子倔监,這個(gè)程序?qū)⒋蛴∫粋€(gè)數(shù)字的個(gè)位數(shù)的平方和直砂。

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)
}

運(yùn)行結(jié)果:

Final output 1536

1.3.4 死鎖

使用通道時(shí)要考慮的一個(gè)重要因素是死鎖菌仁。如果Goroutine在一個(gè)通道上發(fā)送數(shù)據(jù),那么預(yù)計(jì)其他的Goroutine應(yīng)該接收數(shù)據(jù)静暂。如果這種情況不發(fā)生济丘,那么程序?qū)⒃谶\(yùn)行時(shí)出現(xiàn)死鎖。

類似地洽蛀,如果Goroutine正在等待從通道接收數(shù)據(jù)摹迷,那么另一些Goroutine將會(huì)在該通道上寫入數(shù)據(jù),否則程序?qū)?huì)死鎖郊供。

示例代碼:

package main

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

報(bào)錯(cuò):

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
    /tmp/sandbox249677995/main.go:6 +0x80

1.3.5 定向通道

之前我們學(xué)習(xí)的通道都是雙向通道峡碉,我們可以通過這些通道接收或者發(fā)送數(shù)據(jù)。我們也可以創(chuàng)建單向通道驮审,這些通道只能發(fā)送或者接收數(shù)據(jù)鲫寄。

創(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)
}

示例代碼:

package main

import "fmt"

func sendData(sendch chan<- int) {
    sendch <- 10
}

func main() {
    chnl := make(chan int)
    go sendData(chnl)
    fmt.Println(<-chnl)
}

1.3.6 關(guān)閉通道和通道上的范圍循環(huán)

發(fā)送者可以通過關(guān)閉信道疯淫,來(lái)通知接收方不會(huì)有更多的數(shù)據(jù)被發(fā)送到信道上地来。

接收者可以在接收來(lái)自通道的數(shù)據(jù)時(shí)使用額外的變量來(lái)檢查通道是否已經(jīng)關(guān)閉。

語(yǔ)法結(jié)構(gòu):

v, ok := <- ch

在上面的語(yǔ)句中熙掺,如果ok的值是true未斑,表示成功的將value值發(fā)送到一個(gè)通道。如果ok是false币绩,這意味著我們正在從一個(gè)封閉的通道讀取數(shù)據(jù)蜡秽。從閉通道讀取的值將是通道類型的零值府阀。

例如,如果通道是一個(gè)int通道芽突,那么從封閉通道接收的值將為0肌似。

示例代碼:

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)
    }
}

運(yùn)行結(jié)果

Received  0 true
Received  1 true
Received  2 true
Received  3 true
Received  4 true
Received  5 true
Received  6 true
Received  7 true
Received  8 true
Received  9 true

在上面的程序中,producer Goroutine將0到9寫入chnl通道诉瓦,然后關(guān)閉通道川队。主函數(shù)里有一個(gè)無(wú)限循環(huán)。它檢查通道是否在行號(hào)中使用變量ok關(guān)閉睬澡。如果ok是假的固额,則意味著通道關(guān)閉,因此循環(huán)結(jié)束煞聪。還可以打印接收到的值和ok的值斗躏。for循環(huán)的for range形式可用于從通道接收值,直到它關(guān)閉為止昔脯。

使用range循環(huán)啄糙,示例代碼:

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)
    }
}

1.4 緩沖通道和工作池

之前學(xué)習(xí)的所有通道基本上都沒有緩沖。發(fā)送和接收到一個(gè)未緩沖的通道是阻塞的云稚。

可以用緩沖區(qū)創(chuàng)建一個(gè)通道隧饼。發(fā)送到一個(gè)緩沖通道只有在緩沖區(qū)滿時(shí)才被阻塞。類似地静陈,從緩沖通道接收的信息只有在緩沖區(qū)為空時(shí)才會(huì)被阻塞燕雁。

可以通過將額外的容量參數(shù)傳遞給make函數(shù)來(lái)創(chuàng)建緩沖通道,該函數(shù)指定緩沖區(qū)的大小鲸拥。

語(yǔ)法:

ch := make(chan type, capacity)

上述語(yǔ)法的容量應(yīng)該大于0拐格,以便通道具有緩沖區(qū)。默認(rèn)情況下刑赶,無(wú)緩沖通道的容量為0捏浊,因此在之前創(chuàng)建通道時(shí)省略了容量參數(shù)。

示例代碼:

package main

import (
    "fmt"
)


func main() {
    ch := make(chan string, 2)
    ch <- "naveen"
    ch <- "paul"
    fmt.Println(<- ch)
    fmt.Println(<- ch)
}

原文:第14章-并發(fā)性Concurrency
作者:黎躍春

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末撞叨,一起剝皮案震驚了整個(gè)濱河市金踪,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌谒所,老刑警劉巖热康,帶你破解...
    沈念sama閱讀 211,123評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異劣领,居然都是意外死亡姐军,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)奕锌,“玉大人著觉,你說(shuō)我怎么就攤上這事【” “怎么了饼丘?”我有些...
    開封第一講書人閱讀 156,723評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)辽话。 經(jīng)常有香客問我肄鸽,道長(zhǎng),這世上最難降的妖魔是什么油啤? 我笑而不...
    開封第一講書人閱讀 56,357評(píng)論 1 283
  • 正文 為了忘掉前任典徘,我火速辦了婚禮,結(jié)果婚禮上益咬,老公的妹妹穿的比我還像新娘逮诲。我一直安慰自己,他們只是感情好幽告,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,412評(píng)論 5 384
  • 文/花漫 我一把揭開白布梅鹦。 她就那樣靜靜地躺著,像睡著了一般冗锁。 火紅的嫁衣襯著肌膚如雪齐唆。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,760評(píng)論 1 289
  • 那天蒿讥,我揣著相機(jī)與錄音蝶念,去河邊找鬼抛腕。 笑死芋绸,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的担敌。 我是一名探鬼主播摔敛,決...
    沈念sama閱讀 38,904評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼全封!你這毒婦竟也來(lái)了马昙?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,672評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤刹悴,失蹤者是張志新(化名)和其女友劉穎行楞,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體土匀,經(jīng)...
    沈念sama閱讀 44,118評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡子房,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,456評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片证杭。...
    茶點(diǎn)故事閱讀 38,599評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡田度,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出解愤,到底是詐尸還是另有隱情镇饺,我是刑警寧澤,帶...
    沈念sama閱讀 34,264評(píng)論 4 328
  • 正文 年R本政府宣布送讲,位于F島的核電站奸笤,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏哼鬓。R本人自食惡果不足惜揭保,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,857評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望魄宏。 院中可真熱鬧秸侣,春花似錦、人聲如沸宠互。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)予跌。三九已至搏色,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間券册,已是汗流浹背频轿。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留烁焙,地道東北人航邢。 一個(gè)月前我還...
    沈念sama閱讀 46,286評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像骄蝇,于是被迫代替她去往敵國(guó)和親膳殷。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,465評(píng)論 2 348

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