由於併發程序要考慮不少的細節,以保證對共享變量的正確訪問,使得併發編程在不少狀況下變得很複雜。
可是Go語言在開發併發時,是比較簡潔的。它經過channel來傳遞數據。數據競爭這個問題在golang的設計上就進行了規避了。它提倡用通訊的方式實現共享,而不要以共享方式來通訊
Go語言用2種手段來實現併發程序,goroutine和channel,其支持順序通訊進程(communicating sequential processes),簡稱爲CSP。CSP是一種現代的併發編程模型,在這種編程模型中,值會在不一樣的運行實例(goroutine)中傳遞。golang
在Go語言中,每個併發的執行單元就叫作goroutine。
每一個goroutine都對應一個很是簡單的模型:它是一個併發的執行函數,而且在多個併發的goroutine間,資源是共享的。goroutine很是輕量,建立的開銷不多。編程
goroutine的用法:
直接在函數前加上一個關鍵字:go。
go func() {}bash
例子:數據結構
package main import ( "fmt" "time" ) func main() { fmt.Println("In main") go longSleep() go shortSleep() fmt.Println("sleep ") time.Sleep(10 * 1e9)//ns,符號 1e9 表示 1 乘 10 的 9 次方,e=指數 fmt.Println("the end of main") } func longSleep() { fmt.Println("longSleep begin") time.Sleep(5 * 1e9) fmt.Println("longSleep end") } func shortSleep() { fmt.Println("shortSleep begin") time.Sleep(2 * 1e9) fmt.Println("shortSleep end") }
運行結果:併發
In main sleep longSleep begin shortSleep begin shortSleep end longSleep end the end of main
main() ,longSleep() 和 shortSleep() 這3個函數都是獨立的處理單元按順序啓動,而後開始並行運行。爲了模擬
運算時間的損耗,咱們使用了sleep()函數,這個函數能夠按照指定時間來暫停函數或協程執行。app
若是咱們不在main()函數中sleep()較長的時間,那麼main() 函數結束時,其餘協程運行的程序也會結束。main()程序退出,它不會等待任何其餘非main協程的結束。
協程是獨立的處理單元,一旦陸續啓動一些協程,就沒法肯定他們是何時正在開始運行的。函數
上面咱們講到,協程都是獨立運行的,他們之間沒有通訊。
協程可使用共享變量來通訊,可是不建議這麼作。在Go中有一種特殊的類型channle通道,能夠經過它來進行goroutine之間的通訊,能夠避免共享內存的坑。channel的通訊保證了同步性。
數據經過通道,同一時間只有一個協程能夠訪問數據,因此不會出現數據競爭,設計時就是這樣的。ui
channel也是經過make進行分配的,其返回的是指向底層相關數據結構的引用。設計
var chan1 chan string chan1 = make(chan string) //or chan1 := make(chan string) //int intchan := make(chan int) //函數也能夠 funcchan := chan func()
var chan2 chan string chan2 := make(chan string) chan3 := make(chan string, 0)
//在make第二個參數加上數字,就變成一個帶緩衝的channel, //也是一個雙向channel,既能夠讀也能夠寫 chan3 := make(chan string, 4)
//只發送的channel,在類型後面加上一個箭頭 <-,只能向channel寫數據 var chan4 chan <-int chan4 := make(chan <-int)
//只接收的channel,箭頭放在chan前面,只能從channel讀取數據 var chan4 <-chan int chan4 := make(<-chan int) //初始化
基礎特性code
操做 | 值爲 nil 的 channel | 被關閉的 channel | 正常的 channel |
---|---|---|---|
close | panic | panic | 成功關閉 |
c<- | 永遠阻塞 | panic | 阻塞或成功發送 |
<-c | 永遠阻塞 | 永遠不阻塞 | 阻塞或成功接收 |
happens-before 特性
channel無緩衝區,發送方和接收方須要一一配對,否則發送方會一直阻塞,直到數據被接收方取出。
其實無緩衝區channel不論是存消息仍是取消息,都會掛起當前goroutine,除非另一端已經準備好。
無緩衝區的channel永遠不會存數據,只負責數據的流通。
注意:
同步的channel不能只在一個協程中發送和接收,由於會被永遠阻塞,數據不能到接收方那裏。
package main import "fmt" func main() { chan1 := make(chan int) go func() { for d := range chan1 { fmt.Println(d) } }() chan1 <- 1 //發送要放在接收協程跑起來後面,由於發送後會阻塞等待接收 chan1 <- 2 chan1 <- 3 close(chan1) }
import "fmt" func sum(s []int, c chan int) { sum := 0 for _, v := range s { sum += v } c <- sum // send sum to c } func main() { s := []int{7, 2, 8, -9, 4, 0} c := make(chan int) go sum(s[:len(s)/2], c) go sum(s[len(s)/2:], c) x, y := <-c, <-c // receive from c fmt.Println(x, y, x+y) }
有緩衝區
channel建立一個緩衝區,若是緩衝區已滿,發送方的主進程或者協程會被阻塞,發送方只能在接收方取走數據後才能從阻塞狀態恢復;若是未滿就不會阻塞;若是爲空,接收方的協程會被阻塞。
上面的這種特性,好比能夠控制主進程的退出,由於有時咱們碰到主協程退出了,其餘的子協程尚未運行完成。
package main import ( "fmt" ) //------------- var ichan = make(chan int, 3) var str string func f() { str = "hello world" ichan <- 0 } func main() { go f() <-ichan //這裏有值,下面纔會運行 fmt.Println(str) }
package main import ( "fmt" ) func main() { chan1 := make(chan int, 3) quit := make(chan bool) //阻塞主進程,防止未處理完的子協程 go func() { for d := range chan1 { //若是data的緩衝區爲空,這個協程會一直阻塞,除非被channel被close fmt.Println(d) } quit <- true }() chan1 <- 1 chan1 <- 2 chan1 <- 3 chan1 <- 4 chan1 <- 5 close(chan1) //用完須要關閉,不然goroutine會被死鎖,由於上面用range,它是不等到信道關閉是不會結束讀取的 <-quit //解除阻塞 }
上面有的例子是一個一個的取數據,其實golang還提供了for range 來讀取channel中的數據。
package main import ( "fmt" "time" ) func main() { go func() { time.Sleep(1 * time.Hour) }() c := make(chan int) go func() { for i := 0; i < 10; i = i + 1 { c <- i } close(c)//若是把close(c)註釋掉,程序會一直阻塞在for …… range那一行 }() for i := range c { fmt.Println(i) } fmt.Println("end!") } //range c 產生的迭代值爲channel中發送的值,它會一直迭代直到channel被關閉。 //注意:上面的例子中若是把close(c)註釋掉,程序會一直阻塞在for …… range那一行
select監測各個channel的數據。
若是有多個channel接收數據,select會隨機選擇一個case來處理。
你還能夠給select加上一個default語句,若是沒有case須要處理,那麼就會選擇default語句。
多個case狀況下,若是沒有default也沒有case須要處理的,那麼select會阻塞,只到某個case須要處理。
注意:nil channel 的操做會一直被阻塞,若是沒有default的話,select會一直被阻塞。
package main import ( "fmt" ) func foo(i int) chan int { c := make(chan int) go func() { c <- i }() return c } func main() { c1, c2, c3 := foo(1), foo(2), foo(3) ichan := make(chan int) //開一個goroutine監聽各個channel數據輸出並收集數據到channel go func() { for {//for語句循環處理select, 若是隻有一個select,那麼它只會選一個case處理就結束了 select { //監聽c1,c2,c3流出,並所有流入到ichan case v1 := <-c1: ichan <- v1 case v2 := <-c2: ichan <- v2 case v3 := <-c3: ichan <- v3 } } }() //阻塞主協程,取出ichan的數據 for i := 0; i < 3; i++ { fmt.Println(<-ichan) // 從打印來看咱們的數據輸出並非嚴格的1,2,3順序 } fmt.Println("end!") }
輸出結果:
2 1 3 end!
select還有一個應用超時處理的功能。上面說到若是沒有case須要處理,那麼select會一直阻塞,這時候咱們就能夠在一個case下定義一個超時狀況,其餘case沒有數據處理時,到時間點了這個超時case就會處理了,就不會一直阻塞。
咱們用time.After,它返回一個類型爲 <-chan time 的單向channel,在指定時間發送一個當前時間給channel
package main import ( "fmt" "time" ) func main() { chan1 := make(chan string, 1) go func() { time.Sleep(time.Second * 3) chan1 <- "res1" }() select { case res := <-chan1: //3秒以後纔會有數據進入槽chan1 fmt.Println(res) case <-time.After(time.Second * 1)://定義超時狀況,1秒後超時.這個超時時間比上面的case短,因此先運行這個case fmt.Println("timeout 1") } }
輸出:
timeout 1
上面的特性咱們列舉了close channel的狀況。
close()掉了,你繼續往裏面寫數據,會出現panic。
可是,從這個關閉的channel能夠讀出已發送的數據,還能夠不斷的讀取零值。
若是是經過range讀取數據,channel關閉後for循環會跳出。
經過i, ok := <-c 能夠查看channel的狀態,判斷是零值仍是正常讀取的值。
c := make(chan int, 10) close(c) i, ok := <-c fmt.Printf("%d, %t", i, ok) //0, false