有人把Go比作21世紀(jì)的C語言,第一是因為Go語言設(shè)計簡單,第二,21世紀(jì)最重要的就是并行程序設(shè)計,而Go從語言層面就支持了并行。
goroutine是Go并行設(shè)計的核心。goroutine說到底其實就是線程,但是它比線程更小,十幾個goroutine可能體現(xiàn)在底層就是五六個線程,Go語言內(nèi)部幫你實現(xiàn)了這些goroutine之間的內(nèi)存共享。執(zhí)行g(shù)oroutine只需極少的棧內(nèi)存(大概是4~5KB),當(dāng)然會根據(jù)相應(yīng)的數(shù)據(jù)伸縮。也正因為如此,可同時運行成千上萬個并發(fā)任務(wù)。goroutine比thread更易用、更高效、更輕便。
goroutine是通過Go的runtime管理的一個線程管理器。goroutine通過go
關(guān)鍵字實現(xiàn)了,其實就是一個普通的函數(shù)。
go hello(a, b, c)
通過關(guān)鍵字go就啟動了一個goroutine。我們來看一個例子
package main
import (
"fmt"
"runtime"
)
func say(s string) {
for i := 0; i < 5; i++ {
runtime.Gosched()
fmt.Println(s)
}
}
func main() {
go say("world") //開一個新的Goroutines執(zhí)行
say("hello") //當(dāng)前Goroutines執(zhí)行
}
// 以上程序執(zhí)行后將輸出:
// hello
// world
// hello
// world
// hello
// world
// hello
// world
// hello
我們可以看到go關(guān)鍵字很方便的就實現(xiàn)了并發(fā)編程。 上面的多個goroutine運行在同一個進(jìn)程里面,共享內(nèi)存數(shù)據(jù),不過設(shè)計上我們要遵循:不要通過共享來通信,而要通過通信來共享。
runtime.Gosched()表示讓CPU把時間片讓給別人,下次某個時候繼續(xù)恢復(fù)執(zhí)行該goroutine。
默認(rèn)情況下,調(diào)度器僅使用單線程,也就是說只實現(xiàn)了并發(fā)。想要發(fā)揮多核處理器的并行,需要在我們的程序中顯式調(diào)用 runtime.GOMAXPROCS(n) 告訴調(diào)度器同時使用多個線程。GOMAXPROCS 設(shè)置了同時運行邏輯代碼的系統(tǒng)線程的最大數(shù)量,并返回之前的設(shè)置。如果n < 1,不會改變當(dāng)前設(shè)置。以后Go的新版本中調(diào)度得到改進(jìn)后,這將被移除。這里有一篇Rob介紹的關(guān)于并發(fā)和并行的文章: http://concur.rspace.googlecode.com/hg/talk/concur.html#landing-slide
goroutine運行在相同的地址空間,因此訪問共享內(nèi)存必須做好同步。那么goroutine之間如何進(jìn)行數(shù)據(jù)的通信呢,Go提供了一個很好的通信機制channel。channel可以與Unix shell 中的雙向管道做類比:可以通過它發(fā)送或者接收值。這些值只能是特定的類型:channel類型。定義一個channel時,也需要定義發(fā)送到channel的值的類型。注意,必須使用make 創(chuàng)建channel:
ci := make(chan int)
cs := make(chan string)
cf := make(chan interface{})
channel通過操作符<-
來接收和發(fā)送數(shù)據(jù)
ch <- v // 發(fā)送v到channel ch.
v := <-ch // 從ch中接收數(shù)據(jù),并賦值給v
我們把這些應(yīng)用到我們的例子中來:
package main
import "fmt"
func sum(a []int, c chan int) {
total := 0
for _, v := range a {
total += v
}
c <- total // send total to c
}
func main() {
a := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(a[:len(a)/2], c)
go sum(a[len(a)/2:], c)
x, y := <-c, <-c // receive from c
fmt.Println(x, y, x + y)
}
默認(rèn)情況下,channel接收和發(fā)送數(shù)據(jù)都是阻塞的,除非另一端已經(jīng)準(zhǔn)備好,這樣就使得Goroutines同步變的更加的簡單,而不需要顯式的lock。所謂阻塞,也就是如果讀?。╲alue := <-ch)它將會被阻塞,直到有數(shù)據(jù)接收。其次,任何發(fā)送(ch<-5)將會被阻塞,直到數(shù)據(jù)被讀出。無緩沖channel是在多個goroutine之間同步很棒的工具。
上面我們介紹了默認(rèn)的非緩存類型的channel,不過Go也允許指定channel的緩沖大小,很簡單,就是channel可以存儲多少元素。ch:= make(chan bool, 4),創(chuàng)建了可以存儲4個元素的bool 型channel。在這個channel 中,前4個元素可以無阻塞的寫入。當(dāng)寫入第5個元素時,代碼將會阻塞,直到其他goroutine從channel 中讀取一些元素,騰出空間。
ch := make(chan type, value)
value == 0 ! 無緩沖(阻塞)
value > 0 ! 緩沖(非阻塞,直到value 個元素)
我們看一下下面這個例子,你可以在自己本機測試一下,修改相應(yīng)的value值
package main
import "fmt"
func main() {
c := make(chan int, 2)//修改2為1就報錯,修改2為3可以正常運行
c <- 1
c <- 2
fmt.Println(<-c)
fmt.Println(<-c)
}
//修改為1報如下的錯誤:
//fatal error: all goroutines are asleep - deadlock!
上面這個例子中,我們需要讀取兩次c,這樣不是很方便,Go考慮到了這一點,所以也可以通過range,像操作slice或者map一樣操作緩存類型的channel,請看下面的例子
package main
import (
"fmt"
)
func fibonacci(n int, c chan int) {
x, y := 1, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x + y
}
close(c)
}
func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
for i := range c {
fmt.Println(i)
}
}
for i := range c
能夠不斷的讀取channel里面的數(shù)據(jù),直到該channel被顯式的關(guān)閉。上面代碼我們看到可以顯式的關(guān)閉channel,生產(chǎn)者通過內(nèi)置函數(shù)close
關(guān)閉channel。關(guān)閉channel之后就無法再發(fā)送任何數(shù)據(jù)了,在消費方可以通過語法v, ok := <-ch
測試channel是否被關(guān)閉。如果ok返回false,那么說明channel已經(jīng)沒有任何數(shù)據(jù)并且已經(jīng)被關(guān)閉。
記住應(yīng)該在生產(chǎn)者的地方關(guān)閉channel,而不是消費的地方去關(guān)閉它,這樣容易引起panic
另外記住一點的就是channel不像文件之類的,不需要經(jīng)常去關(guān)閉,只有當(dāng)你確實沒有任何發(fā)送數(shù)據(jù)了,或者你想顯式的結(jié)束range循環(huán)之類的
我們上面介紹的都是只有一個channel的情況,那么如果存在多個channel的時候,我們該如何操作呢,Go里面提供了一個關(guān)鍵字select
,通過select
可以監(jiān)聽channel上的數(shù)據(jù)流動。
select
默認(rèn)是阻塞的,只有當(dāng)監(jiān)聽的channel中有發(fā)送或接收可以進(jìn)行時才會運行,當(dāng)多個channel都準(zhǔn)備好的時候,select是隨機的選擇一個執(zhí)行的。
package main
import "fmt"
func fibonacci(c, quit chan int) {
x, y := 1, 1
for {
select {
case c <- x:
x, y = y, x + y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}
在select
里面還有default語法,select
其實就是類似switch的功能,default就是當(dāng)監(jiān)聽的channel都沒有準(zhǔn)備好的時候,默認(rèn)執(zhí)行的(select不再阻塞等待channel)。
select {
case i := <-c:
// use i
default:
// 當(dāng)c阻塞的時候執(zhí)行這里
}
有時候會出現(xiàn)goroutine阻塞的情況,那么我們?nèi)绾伪苊庹麄€程序進(jìn)入阻塞的情況呢?我們可以利用select來設(shè)置超時,通過如下的方式實現(xiàn):
func main() {
c := make(chan int)
o := make(chan bool)
go func() {
for {
select {
case v := <- c:
println(v)
case <- time.After(5 * time.Second):
println("timeout")
o <- true
break
}
}
}()
<- o
}
runtime包中有幾個處理goroutine的函數(shù):
Goexit
退出當(dāng)前執(zhí)行的goroutine,但是defer函數(shù)還會繼續(xù)調(diào)用
Gosched
讓出當(dāng)前goroutine的執(zhí)行權(quán)限,調(diào)度器安排其他等待的任務(wù)運行,并在下次某個時候從該位置恢復(fù)執(zhí)行。
NumCPU
返回 CPU 核數(shù)量
NumGoroutine
返回正在執(zhí)行和排隊的任務(wù)總數(shù)
GOMAXPROCS
用來設(shè)置可以并行計算的CPU核數(shù)的最大值,并返回之前的值。
更多建議: