并發(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ì)。
與線程相比雷厂,Goroutines非常便宜惋增。它們只是堆棧大小的幾個(gè)kb,堆椄啮辏可以根據(jù)應(yīng)用程序的需要增長(zhǎng)和收縮诈皿,而在線程的情況下林束,堆棧大小必須指定并且是固定的
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瘪松。
當(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ī)則
當(dāng)新的Goroutine開始時(shí)桐智,Goroutine調(diào)用立即返回。與函數(shù)不同烟馅,go不等待Goroutine執(zhí)行結(jié)束酵使。當(dāng)Goroutine調(diào)用,并且Goroutine的任何返回值被忽略之后焙糟,go立即執(zhí)行到下一行代碼口渔。
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
作者:黎躍春