Golang學習筆記之併發.協程(Goroutine)、通道(Channel)
Go是併發語言,而不是並行語言。
一、併發和並行的區別
•併發(concurrency)是指一次處理大量事情的能力。併發的關鍵是你有處理多個任務的能力,不一定要同時。
•並行(parallelism)指的是同時處理多個事情。並行的關鍵是你有同時處理多個任務的能力。
簡單的理解一下,併發就是你在跑步的時候鞋帶開了,你停下來繫鞋帶。而並行則是,你一邊聽歌一邊跑步。
並行並不代表比並發快,舉一個例子,當檔案下載完成時,應該使用彈出視窗來通知使用者。而這種通訊發生在負責下載的元件和負責渲染使用者介面的元件之間。在併發系統中,這種通訊的開銷很低。而如果這兩個元件並行地執行在 CPU 的不同核上,這種通訊的開銷卻很大。因此並行程式並不一定會執行得更快。
Go 原生支援併發。在Go中,使用 Go 協程(Goroutine)和通道(channel)來處理併發。
二、Go協程(Goroutine)
只需在函式調⽤語句前新增 go 關鍵字,就可建立併發執⾏單元。開發⼈員⽆需瞭解任何執⾏細節,排程器會⾃動將其安排到合適的系統執行緒上執⾏。協程是⼀種⾮常輕量級的實現,可在單個程序⾥執⾏成千上萬的併發任務。
•排程器不能保證多個 goroutine 執⾏次序,且程序退出時不會等待它們結束。
•Go 協程之間通過通道(channel)進行通訊。
•協程裡可以建立協程
(1)協程的建立
package main import ( "fmt" "time" ) func hello() { fmt.Println("Hello world goroutine") } func main() { //開啟了一個新的協程。hello() 函式將和 main() 函式一起執行。 go hello() time.Sleep(1 * time.Second) //延時結束主程式,不然不能保證主程式會等協程程式 fmt.Println("main function") }
(2)建立多個協程
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() { //numbers()和alphabets()併發執行 go numbers() go alphabets() time.Sleep(3000 * time.Millisecond) fmt.Println("Main Over") }
輸出:
1 a 2 3 b 4 c 5 d e Main Over
(3)調⽤ runtime.Goexit()將⽴即終⽌當前 goroutine 執⾏。但所有已註冊 defer延遲調⽤會被被執⾏。
修改一下上面的程式碼
func alphabets() { defer fmt.Println("結束") //defer會被呼叫 for i := 'a'; i <= 'e'; i++ { time.Sleep(400 * time.Millisecond) fmt.Printf("%c ", i) runtime.Goexit() //立即結束該協程 } }
程式輸出則會變為
1 a 結束
2 3 4 5 Main Over
(4)調⽤ runtime.Gosched()將當前 goroutine 暫停,放回佇列等待下次被排程執⾏。
package main import ( "fmt" "runtime" ) func main() { go func() { //子協程//沒來的及執行主程序結束 for i := 0; i < 5; i++ { fmt.Println(i) } }() for i := 0; i < 2; i++ { //預設先執行主程序主程序執行完畢 //讓出時間片,先讓別的協議執行,它執行完,再回來執行此協程 runtime.Gosched() fmt.Println("執行") } }
三、通道(Channel)
通道(Channel)可以被認為是協程之間通訊的管道。資料可以從通道的一端傳送並在另一端接收。
•預設為同步模式,需要傳送和接收配對。否則會被阻塞,直到另⼀⽅準備好後被喚醒。
•通道支援單向通道
通道宣告
var ch chan T
我們聲明瞭一個T型別的名稱叫做ch的通道
通道的 0 值為 nil。我們需要通過內建函式 make 來建立一個通道,就像建立 map 和 slice 一樣。
ch := make(chan T)
(1)通道的建立
//內建型別channel var a chan int if a == nil { a = make(chan int) fmt.Printf("%T\n", a) //chan int } //自定義型別channel var p chan person if p == nil { p = make(chan person) //chan main.person fmt.Printf("%T\n", p) }
(2)通過通道傳送和接收資料
data := <- a // 從通道 a 中讀取資料並將讀取的值賦值給變數 data 。 a <- data // 向通道 a 中寫入資料。
(3)傳送和接收預設是阻塞的
package main import ( "fmt" "time" ) func hello(done chan bool) { fmt.Println("hello go routine is going to sleep") time.Sleep(4 * time.Second) //只有寫資料後才能繼續執行 done <- true fmt.Println("hello go routine awake and going to write to done") } func main() { done := make(chan bool) go hello(done) <-done fmt.Println("Main received data") }
(4)死鎖
使用通道是要考慮的一個重要因素是死鎖(Deadlock)只讀未寫與只寫未讀都會觸發死鎖,並觸發 panic 。
channel 上如果發生了流入和流出不配對,就可能會發生死鎖。
package main func main() { ch := make(chan int) ch <- 5//只寫未讀觸發死鎖 }
(6)單向通道與關閉通道close()
傳送者可以關閉通道以通知接收者將不會再發送資料給通道。
v, ok := <- ch
判斷通道是否已關閉
package main import ( "fmt" ) //只寫操作 func sendData(sendch chan<- int) { sendch <- 10 //不能讀 //<-sendch close(sendch) //顯式關閉通道 } //只讀操作 func readData(sendch <-chan int) { <-sendch } func main() { sendch := make(chan int) go sendData(sendch) v, ok := <-sendch //ok 返回 true 表示成功的接收到了傳送的資料,如果 ok 返回 false 則表示通道已經被關閉。 v1, ok1 := <-sendch fmt.Println(v, ok)//10 true fmt.Println(v1, ok1) //0 false }
(7)遍歷通道
通道支援range for遍歷
package main import ( "fmt" "time" ) func producer(chnl chan int) { defer close(chnl) //程式執行結束關閉通道 for i := 0; i < 10; i++ { time.Sleep(300 * time.Millisecond) //一秒寫一次 chnl <- i//寫操作 } } func main() { ch := make(chan int) go producer(ch) //接收ch通道中的資料,直到該通道關閉。 for v := range ch { fmt.Println(v) } }
也可以自定for迴圈遍歷通道
for { v, ok := <-ch //讀操作 fmt.Println(v, ok) if ok == false { //當讀取不到資料跳出迴圈 break } }
(8)緩衝通道
語法結構
ch := make(chan type, cap)
cap為容量。
•緩衝通道支援len()和cap()
•只能先緩衝通道寫容量以內的資料
•只能讀緩衝通道長度以內的資料
func main() { //建立一個容量為3的緩衝通道 ch := make(chan string, 3) ch <- "naveen" ch <- "paul" fmt.Println("capacity is", cap(ch))//capacity is 3 fmt.Println("length is", len(ch))//length is 2 fmt.Println("read value", <-ch)//read value naveen fmt.Println("new length is", len(ch)) //new length is 1 }
(9)WaitGroup
假設我們有 3 個併發執行的 Go 協程(由Go 主協程生成)。Go 主協程需要等待這 3 個協程執行結束後,才會終止。這就可以用 WaitGroup 來實現。
package main import ( "fmt" "sync" "time" ) func process(i int, wg *sync.WaitGroup) { fmt.Println("started Goroutine ", i) time.Sleep(2 * time.Second) fmt.Printf("Goroutine %d ended\n", i) //Done方法減少WaitGroup計數器的值,應線上程的最後執行。 wg.Done() } /* WaitGroup用於等待一組執行緒的結束。 父執行緒呼叫Add方法來設定應等待的執行緒的數量。 每個被等待的執行緒在結束時應呼叫Done方法。 同時,主執行緒裡可以呼叫Wait方法阻塞至所有執行緒結束。 */ func main() { no := 3 var wg sync.WaitGroup //併發協程 for i := 0; i < no; i++ { /* Add方法向內部計數加上delta,delta可以是負數; 如果內部計數器變為0,Wait方法阻塞等待的所有執行緒都會釋放, 如果計數器小於0,方法panic。 */ wg.Add(1) go process(i, &wg) } //Wait方法阻塞直到WaitGroup計數器減為0。 wg.Wait() fmt.Println("over") }
(9)select
select 語句用於在多個傳送/接收通道操作中進行選擇。select 語句會一直阻塞,直到傳送/接收操作準備就緒。
•如果有多個通道操作準備完畢,select 會隨機地選取其中之一執行。
•空的select會觸發死鎖因此它會一直阻塞,導致死鎖。
package main import ( "fmt" "time" ) func server1(ch chan string) { time.Sleep(1 * time.Second) ch <- "from server1" } func server2(ch chan string) { time.Sleep(1 * time.Second) ch <- "from server2" } func main() { output1 := make(chan string) output2 := make(chan string) go server1(output1) go server2(output2) //隨機選擇 select { case s1 := <-output1: fmt.Println(s1) case s2 := <-output2: fmt.Println(s2) } }