##11.1 概述 ###11.1.1 並行和併發 並行(parallel):指在同一時刻,有多條指令在多個處理器上同時執行。 編程
併發(concurrency):指在同一時刻只能有一條指令執行,但多個進程指令被快速的輪換執行,使得在宏觀上具備多個進程同時執行的效果,但在微觀上並非同時執行的,只是把時間分紅若干段,使多個進程快速交替的執行。 安全
λ 並行是兩個隊列同時使用兩臺咖啡機 λ 併發是兩個隊列交替使用一臺咖啡機 bash
###11.1.2 Go語言併發優點 有人把Go比做21世紀的C語言,第一是由於Go語言設計簡單,第二,21世紀最重要的就是並行程序設計,而Go從語言層面就支持了並行。同時,併發程序的內存管理有時候是很是複雜的,而Go語言提供了自動垃圾回收機制。數據結構
Go語言爲併發編程而內置的上層API基於CSP(communicating sequential processes, 順序通訊進程)模型。這就意味着顯式鎖都是能夠避免的,由於Go語言經過相冊安全的通道發送和接受數據以實現同步,這大大地簡化了併發程序的編寫。併發
通常狀況下,一個普通的桌面計算機跑十幾二十個線程就有點負載過大了,可是一樣這臺機器卻能夠輕鬆地讓成百上千甚至過萬個goroutine進行資源競爭。異步
##11.2 goroutine ###11.2.1 goroutine是什麼 goroutine是Go並行設計的核心。goroutine說到底其實就是協程,可是它比線程更小,十幾個goroutine可能體如今底層就是五六個線程,Go語言內部幫你實現了這些goroutine之間的內存共享。執行goroutine只需極少的棧內存(大概是4~5KB),固然會根據相應的數據伸縮。也正由於如此,可同時運行成千上萬個併發任務。goroutine比thread更易用、更高效、更輕便。 ###11.2.2 建立goroutine 只需在函數調⽤語句前添加 go 關鍵字,就可建立併發執⾏單元。開發⼈員無需瞭解任何執⾏細節,調度器會自動將其安排到合適的系統線程上執行。函數
在併發編程裏,咱們一般想講一個過程切分紅幾塊,而後讓每一個goroutine各自負責一塊工做。當一個程序啓動時,其主函數即在一個單獨的goroutine中運行,咱們叫它main goroutine。新的goroutine會用go語句來建立。ui
示例代碼:spa
package main
import (
"fmt"
"time"
)
func newTask() {
i := 0
for {
i++
fmt.Printf("new goroutine: i = %d\n", i)
time.Sleep(1 * time.Second) //延時1s
}
}
func main() {
//建立一個 goroutine,啓動另一個任務
go newTask()
i := 0
//main goroutine 循環打印
for {
i++
fmt.Printf("main goroutine: i = %d\n", i)
time.Sleep(1 * time.Second) //延時1s
}
}
複製代碼
程序運行結果: 操作系統
###11.2.3 主goroutine先退出 主goroutine退出後,其它的工做goroutine也會自動退出:
func newTask() {
i := 0
for {
i++
fmt.Printf("new goroutine: i = %d\n", i)
time.Sleep(1 * time.Second) //延時1s
}
}
func main() {
//建立一個 goroutine,啓動另一個任務
go newTask()
fmt.Println("main goroutine exit")
}
複製代碼
程序運行結果:
###11.2.4 runtime包 ####11.2.4.1 Gosched runtime.Gosched() 用於讓出CPU時間片,讓出當前goroutine的執行權限,調度器安排其餘等待的任務運行,並在下次某個時候從該位置恢復執行。
這就像跑接力賽,A跑了一會碰到代碼runtime.Gosched() 就把接力棒交給B了,A歇着了,B繼續跑。
示例代碼:
func main() {
//建立一個goroutine
go func(s string) {
for i := 0; i < 2; i++ {
fmt.Println(s)
}
}("world")
for i := 0; i < 2; i++ {
runtime.Gosched() //import "runtime"
/*
屏蔽runtime.Gosched()運行結果以下:
hello
hello
沒有runtime.Gosched()運行結果以下:
world
world
hello
hello
*/
fmt.Println("hello")
}
}
複製代碼
####11.2.4.2 Goexit 調用 runtime.Goexit() 將當即終止當前 goroutine 執⾏,調度器確保全部已註冊 defer延遲調用被執行。
示例代碼:
func main() {
go func() {
defer fmt.Println("A.defer")
func() {
defer fmt.Println("B.defer")
runtime.Goexit() // 終止當前 goroutine, import "runtime"
fmt.Println("B") // 不會執行
}()
fmt.Println("A") // 不會執行
}() //別忘了()
//死循環,目的不讓主goroutine結束
for {
}
}
複製代碼
程序運行結果:
####11.2.4.3 GOMAXPROCS 調用 runtime.GOMAXPROCS() 用來設置能夠並行計算的CPU核數的最大值,並返回以前的值。
示例代碼:
func main() {
//n := runtime.GOMAXPROCS(1) //打印結果:111111111111111111110000000000000000000011111...
n := runtime.GOMAXPROCS(2) //打印結果:010101010101010101011001100101011010010100110...
fmt.Printf("n = %d\n", n)
for {
go fmt.Print(0)
fmt.Print(1)
}
}
複製代碼
在第一次執行(runtime.GOMAXPROCS(1))時,最多同時只能有一個goroutine被執行。因此 會打印不少1。過了一段時間後,GO調度器會將其置爲休眠,並喚醒另外一個goroutine,這時候就開始打印不少0了,在打印的時候,goroutine是被調度到操做系統線程上的。
在第二次執行(runtime.GOMAXPROCS(2))時,咱們使用了兩個CPU,因此兩個goroutine能夠一塊兒被執行,以一樣的頻率交替打印0和1。
##11.3 channel goroutine運行在相同的地址空間,所以訪問共享內存必須作好同步。goroutine 奉行經過通訊來共享內存,而不是共享內存來通訊。
引⽤類型 channel 是 CSP 模式的具體實現,用於多個 goroutine 通信。其內部實現了同步,確保併發安全。 ###11.3.1 channel類型 和map相似,channel也一個對應make建立的底層數據結構的引用。
當咱們複製一個channel或用於函數參數傳遞時,咱們只是拷貝了一個channel引用,所以調用者何被調用者將引用同一個channel對象。和其它的引用類型同樣,channel的零值也是nil。
定義一個channel時,也須要定義發送到channel的值的類型。channel可使用內置的make()函數來建立:
make(chan Type) //等價於make(chan Type, 0)
make(chan Type, capacity)
複製代碼
當 capacity= 0 時,channel 是無緩衝阻塞讀寫的,當capacity> 0 時,channel 有緩衝、是非阻塞的,直到寫滿 capacity個元素才阻塞寫入。
channel經過操做符<-來接收和發送數據,發送和接收數據語法: channel <- value //發送value到channel <-channel //接收並將其丟棄 x := <-channel //從channel中接收數據,並賦值給x x, ok := <-channel //功能同上,同時檢查通道是否已關閉或者是否爲空
默認狀況下,channel接收和發送數據都是阻塞的,除非另外一端已經準備好,這樣就使得goroutine同步變的更加的簡單,而不須要顯式的lock。
示例代碼:
func main() {
c := make(chan int)
go func() {
defer fmt.Println("子協程結束")
fmt.Println("子協程正在運行……")
c <- 666 //666發送到c
}()
num := <-c //從c中接收數據,並賦值給num
fmt.Println("num = ", num)
fmt.Println("main協程結束")
}
複製代碼
程序運行結果:
###11.3.2 無緩衝的channel 無緩衝的通道(unbuffered channel)是指在接收前沒有能力保存任何值的通道。
這種類型的通道要求發送 goroutine 和接收 goroutine 同時準備好,才能完成發送和接收操做。若是兩個goroutine沒有同時準備好,通道會致使先執行發送或接收操做的 goroutine 阻塞等待。
這種對通道進行發送和接收的交互行爲自己就是同步的。其中任意一個操做都沒法離開另外一個操做單獨存在。
下圖展現兩個 goroutine 如何利用無緩衝的通道來共享一個值:
在第 1 步,兩個 goroutine 都到達通道,但哪一個都沒有開始執行發送或者接收。 在第 2 步,左側的 goroutine 將它的手伸進了通道,這模擬了向通道發送數據的行爲。這時,這個 goroutine 會在通道中被鎖住,直到交換完成。 在第 3 步,右側的 goroutine 將它的手放入通道,這模擬了從通道里接收數據。這個 goroutine 同樣也會在通道中被鎖住,直到交換完成。 在第 4 步和第 5 步,進行交換,並最終,在第 6 步,兩個 goroutine 都將它們的手從通道里拿出來,這模擬了被鎖住的 goroutine 獲得釋放。兩個 goroutine 如今均可以去作別的事情了。
無緩衝的channel建立格式: make(chan Type) //等價於make(chan Type, 0)
若是沒有指定緩衝區容量,那麼該通道就是同步的,所以會阻塞到發送者準備好發送和接收者準備好接收。
示例代碼:
func main() {
c := make(chan int, 0) //無緩衝的通道
//內置函數 len 返回未被讀取的緩衝元素數量, cap 返回緩衝區大小
fmt.Printf("len(c)=%d, cap(c)=%d\n", len(c), cap(c))
go func() {
defer fmt.Println("子協程結束")
for i := 0; i < 3; i++ {
c <- i
fmt.Printf("子協程正在運行[%d]: len(c)=%d, cap(c)=%d\n", i, len(c), cap(c))
}
}()
time.Sleep(2 * time.Second) //延時2s
for i := 0; i < 3; i++ {
num := <-c //從c中接收數據,並賦值給num
fmt.Println("num = ", num)
}
fmt.Println("main協程結束")
}
複製代碼
程序運行結果:
###11.3.3 有緩衝的channel 有緩衝的通道(buffered channel)是一種在被接收前能存儲一個或者多個值的通道。
這種類型的通道並不強制要求 goroutine 之間必須同時完成發送和接收。通道會阻塞發送和接收動做的條件也會不一樣。只有在通道中沒有要接收的值時,接收動做纔會阻塞。只有在通道沒有可用緩衝區容納被髮送的值時,發送動做纔會阻塞。
這致使有緩衝的通道和無緩衝的通道之間的一個很大的不一樣:無緩衝的通道保證進行發送和接收的 goroutine 會在同一時間進行數據交換;有緩衝的通道沒有這種保證。 示例圖以下:
在第 1 步,右側的 goroutine 正在從通道接收一個值。 在第 2 步,右側的這個 goroutine獨立完成了接收值的動做,而左側的 goroutine 正在發送一個新值到通道里。 在第 3 步,左側的goroutine 還在向通道發送新值,而右側的 goroutine 正在從通道接收另一個值。這個步驟裏的兩個操做既不是同步的,也不會互相阻塞。 最後,在第 4 步,全部的發送和接收都完成,而通道里還有幾個值,也有一些空間能夠存更多的值。
有緩衝的channel建立格式: make(chan Type, capacity)
若是給定了一個緩衝區容量,通道就是異步的。只要緩衝區有未使用空間用於發送數據,或還包含能夠接收的數據,那麼其通訊就會無阻塞地進行。
示例代碼:
func main() {
c := make(chan int, 3) //帶緩衝的通道
//內置函數 len 返回未被讀取的緩衝元素數量, cap 返回緩衝區大小
fmt.Printf("len(c)=%d, cap(c)=%d\n", len(c), cap(c))
go func() {
defer fmt.Println("子協程結束")
for i := 0; i < 3; i++ {
c <- i
fmt.Printf("子協程正在運行[%d]: len(c)=%d, cap(c)=%d\n", i, len(c), cap(c))
}
}()
time.Sleep(2 * time.Second) //延時2s
for i := 0; i < 3; i++ {
num := <-c //從c中接收數據,並賦值給num
fmt.Println("num = ", num)
}
fmt.Println("main協程結束")
}
複製代碼
程序運行結果:
###11.3.4 range和close 若是發送者知道,沒有更多的值須要發送到channel的話,那麼讓接收者也能及時知道沒有多餘的值可接收將是有用的,由於接收者能夠中止沒必要要的接收等待。這能夠經過內置的close函數來關閉channel實現。
示例代碼:
func main() {
c := make(chan int)
go func() {
for i := 0; i < 5; i++ {
c <- i
}
//把 close(c) 註釋掉,程序會一直阻塞在 if data, ok := <-c; ok 那一行
close(c)
}()
for {
//ok爲true說明channel沒有關閉,爲false說明管道已經關閉
if data, ok := <-c; ok {
fmt.Println(data)
} else {
break
}
}
fmt.Println("Finished")
}
複製代碼
程序運行結果:
注意點: channel不像文件同樣須要常常去關閉,只有當你確實沒有任何發送數據了,或者你想顯式的結束range循環之類的,纔去關閉channel; 關閉channel後,沒法向channel 再發送數據(引起 panic 錯誤後致使接收當即返回零值); 關閉channel後,能夠繼續向channel接收數據; 對於nil channel,不管收發都會被阻塞。
可使用 range 來迭代不斷操做channel:
func main() {
c := make(chan int)
go func() {
for i := 0; i < 5; i++ {
c <- i
}
//把 close(c) 註釋掉,程序會一直阻塞在 for data := range c 那一行
close(c)
}()
for data := range c {
fmt.Println(data)
}
fmt.Println("Finished")
}
複製代碼
###11.3.5 單方向的channel 默認狀況下,通道是雙向的,也就是,既能夠往裏面發送數據也能夠同裏面接收數據。
可是,咱們常常見一個通道做爲參數進行傳遞而值但願對方是單向使用的,要麼只讓它發送數據,要麼只讓它接收數據,這時候咱們能夠指定通道的方向。
單向channel變量的聲明很是簡單,以下: var ch1 chan int // ch1是一個正常的channel,不是單向的 var ch2 chan<- float64 // ch2是單向channel,只用於寫float64數據 var ch3 <-chan int // ch3是單向channel,只用於讀取int數據
chan<- 表示數據進入管道,要把數據寫進管道,對於調用者就是輸出。 <-chan 表示數據從管道出來,對於調用者就是獲得管道的數據,固然就是輸入。
能夠將 channel 隱式轉換爲單向隊列,只收或只發,不能將單向 channel 轉換爲普通 channel:
c := make(chan int, 3)
var send chan<- int = c // send-only
var recv <-chan int = c // receive-only
send <- 1
//<-send //invalid operation: <-send (receive from send-only type chan<- int)
<-recv
//recv <- 2 //invalid operation: recv <- 2 (send to receive-only type <-chan int)
//不能將單向 channel 轉換爲普通 channel
d1 := (chan int)(send) //cannot convert send (type chan<- int) to type chan int
d2 := (chan int)(recv) //cannot convert recv (type <-chan int) to type chan int
示例代碼:
// chan<- //只寫
func counter(out chan<- int) {
defer close(out)
for i := 0; i < 5; i++ {
out <- i //若是對方不讀 會阻塞
}
}
// <-chan //只讀
func printer(in <-chan int) {
for num := range in {
fmt.Println(num)
}
}
func main() {
c := make(chan int) // chan //讀寫
go counter(c) //生產者
printer(c) //消費者
fmt.Println("done")
}
複製代碼
###11.3.6 定時器 ####11.3.6.1 Timer Timer是一個定時器,表明將來的一個單一事件,你能夠告訴timer你要等待多長時間,它提供一個channel,在未來的那個時間那個channel提供了一個時間值。
示例代碼:
import "fmt"
import "time"
func main() {
//建立定時器,2秒後,定時器就會向本身的C字節發送一個time.Time類型的元素值
timer1 := time.NewTimer(time.Second * 2)
t1 := time.Now() //當前時間
fmt.Printf("t1: %v\n", t1)
t2 := <-timer1.C
fmt.Printf("t2: %v\n", t2)
//若是隻是想單純的等待的話,可使用 time.Sleep 來實現
timer2 := time.NewTimer(time.Second * 2)
<-timer2.C
fmt.Println("2s後")
time.Sleep(time.Second * 2)
fmt.Println("再一次2s後")
<-time.After(time.Second * 2)
fmt.Println("再再一次2s後")
timer3 := time.NewTimer(time.Second)
go func() {
<-timer3.C
fmt.Println("Timer 3 expired")
}()
stop := timer3.Stop() //中止定時器
if stop {
fmt.Println("Timer 3 stopped")
}
fmt.Println("before")
timer4 := time.NewTimer(time.Second * 5) //原來設置3s
timer4.Reset(time.Second * 1) //從新設置時間
<-timer4.C
fmt.Println("after")
}
複製代碼
####11.3.6.2 Ticker Ticker是一個定時觸發的計時器,它會以一個間隔(interval)往channel發送一個事件(當前時間),而channel的接收者能夠以固定的時間間隔從channel中讀取事件。
示例代碼:
func main() {
//建立定時器,每隔1秒後,定時器就會給channel發送一個事件(當前時間)
ticker := time.NewTicker(time.Second * 1)
i := 0
go func() {
for { //循環
<-ticker.C
i++
fmt.Println("i = ", i)
if i == 5 {
ticker.Stop() //中止定時器
}
}
}() //別忘了()
//死循環,特意不讓main goroutine結束
for {
}
}
複製代碼
##11.4 select ###11.4.1 select做用 Go裏面提供了一個關鍵字select,經過select能夠監聽channel上的數據流動。
select的用法與switch語言很是相似,由select開始一個新的選擇塊,每一個選擇條件由case語句來描述。
與switch語句能夠選擇任何可以使用相等比較的條件相比, select有比較多的限制,其中最大的一條限制就是每一個case語句裏必須是一個IO操做,大體的結構以下:
select {
case <-chan1:
// 若是chan1成功讀到數據,則進行該case處理語句
case chan2 <- 1:
// 若是成功向chan2寫入數據,則進行該case處理語句
default:
// 若是上面都沒有成功,則進入default處理流程
}
複製代碼
在一個select語句中,Go語言會按順序從頭到尾評估每個發送和接收的語句。
若是其中的任意一語句能夠繼續執行(即沒有被阻塞),那麼就從那些能夠執行的語句中任意選擇一條來使用。
若是沒有任意一條語句能夠執行(即全部的通道都被阻塞),那麼有兩種可能的狀況: 若是給出了default語句,那麼就會執行default語句,同時程序的執行會從select語句後的語句中恢復。 若是沒有default語句,那麼select語句將被阻塞,直到至少有一個通訊能夠進行下去。
示例代碼:
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 < 6; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}
複製代碼
運行結果以下:
###11.4.2 超時 有時候會出現goroutine阻塞的狀況,那麼咱們如何避免整個程序進入阻塞的狀況呢?咱們能夠利用select來設置超時,經過以下的方式實現:
func main() {
c := make(chan int)
o := make(chan bool)
go func() {
for {
select {
case v := <-c:
fmt.Println(v)
case <-time.After(5 * time.Second):
fmt.Println("timeout")
o <- true
break
}
}
}()
//c <- 666 // 註釋掉,引起 timeout
<-o
}
複製代碼