理解Golang併發程式設計
concurrency vs parallelism
併發和並行是彼此相關的兩個概念,並不能完全等價。在程式中,併發強調的是獨立執行的程式的組合;並行強調的是同時執行計算任務 [1]。
計算機核心的數量決定了平行計算的能力,大多數人類作為 “單核” 動物 (老頑童小龍女除外),可以說自己在併發某些任務,如我在聽歌寫程式碼,但是不能說這兩件事在並行,參考下圖:
Golang 的併發模型源於 Communicating Sequential Processes (CSP),通過提供 goroutine 和 channel 來實現併發程式設計模式。
Goroutine
Goroutine 由 Go 執行時建立和管理,是用於排程 CPU 資源的 “最小單元”,和 OS 的執行緒相比更輕量 [2]:
- 記憶體消耗更低只需 2kB 初始棧空間,而執行緒初始要 1Mb 的空間;
- 由 golang 的執行時環境建立和銷燬,更加廉價,不支援手動管理;
- 切換效率更高等。 Goroutine 和執行緒的關係如下圖所示:
我們可以輕鬆地建立成百上千的 goroutine,而不會降低程式的執行效率。
通過 goroutine 可以讓一個函式和其他的函式並行執行。可以在函式呼叫前面加上go
關鍵字,方便地建立一個 goroutine。
main 函式本身也是一個 goroutine[3]。
舉例如下:
package main
import "fmt"
func main() {
fmt.Println("begin main goroutine")
go hello()
fmt.Println("end main goroutine")
}
func hello() {
fmt.Println("begin hello goroutine")
}
輸出:
begin main goroutine
end main goroutine
上面的例子中,並不會輸出begin hello goroutine
,這是因為,通過使用 goroutine,我們不需要等待函式呼叫的返回結果,而會接著執行下面的程式碼。
可以在go hello()
後面新增:
time.Sleep(1 * time.Second)
就可以正常輸出begin hello goroutine
。
channel
Go 提供了一種機制能夠使 goroutine 之間進行通訊和同步,它就是 channel。
channel 是一種型別,關鍵字chan
和 channel 傳輸內容的型別共同定義了某一 channel。
定義方式為:var c chan string = make(chan string)
,也可以簡寫為:var c = make(chan string)
或 c := make(chan string)
通過左箭頭<-
操作符操作 channel 變數:
-
c <- "ping"
向 channel 傳送一個值為 “ping” 的字串, -
msg := <- c
接收 channel 中的一個值,並賦給 msg。
package main
import (
"fmt"
"strconv"
"time"
)
func main() {
c := make(chan string)
go ping(c)
go print(c)
var input string
fmt.Scanln(&input)
}
func ping(c chan string) {
for i := 0; ; i++ {
c <- strconv.Itoa(i)
}
}
func print(c chan string) {
for {
<-c
fmt.Println("reveving: " + <-c)
time.Sleep(1 * time.Second)
}
}
輸出:
reveving: 1
reveving: 3
reveving: 5
reveving: 7
reveving: 9
...
按功能,可以將 channel 分為只傳送或只接收 channel,通過修改函式簽名的 channel 形參型別來指定 channel 的 “方向”:
- 只允許傳送:
func ping(c chan<- string)
- 只允許接收:
func print(c <-chan string)
- 任何對只傳送 channel 的接收操作和只接收 channel 的傳送操作都會產生編譯錯誤。
- 不指定方向的 channel 被稱作 “雙向” channel,可以將 “雙向” channel 最為引數,傳遞給接收單向 channel 的函式,反之,則不行。
unbuffered channel
非緩衝 channel,也就是緩衝池大小為 0 的 channel 或者同步 channel,上面的例子都是非緩衝 channel,定義方式為:
ch := make(chan int)
ch := make(chan int, 0)
非緩衝 channel 在同步讀時,如果 channel 的 sendq 中有就緒的 goroutine,那麼就取出(copy)資料並釋放傳送方 goroutine;如果沒有就緒的 goroutine,那麼將接收方 goroutine 掛起。
非緩衝 channel 在同步寫時,如果 channel 的 recvq 中有就緒的 goroutine,那麼就取出(copy)資料到接收方 goroutine,並使其就緒;如果沒有,那麼將傳送發 goroutine 掛起。
buffered channel
緩衝 channel 只能容納固定量的資料,當緩衝池滿之後,傳送發被阻塞,直到資料被接收釋放緩衝池,定義如下:
-
ch := make(chan int)
緩衝 channel 可以用來限制吞吐量,例子如下:
package main
import (
"fmt"
"time"
)
// Request struct
type Request struct {
}
var sem = make(chan int, 5) // Create a buffered channel witch capacity of 5
func main() {
queue := make(chan *Request)
go start(queue)
go serve(queue)
var input string
fmt.Scanln(&input)
}
func start(queue chan *Request) {
for {
queue <- &Request{}
}
}
func serve(queue chan *Request) {
for req := range queue {
sem <- 1 // Put on signal to channel
go handle(req) // Don't wait for handle to finish.
}
}
func handle(r *Request) {
process(r) // May take a long time.
<-sem // Done; enable next request to run.
}
func process(r *Request) {
fmt.Println("process")
time.Sleep(4 * time.Second)
}
每隔 4 秒鐘,輸出:
process
process
process
process
process
select
針對於 channel,Golang 提供了一個類似switch
的功能,即select
,使用如下:
-
select
選擇第一個就緒的 channel 進行處理 - 如果有多個就緒的 channel,則隨機選擇一個 channel 進行處理
- 如果沒有就緒的 channel,則等待直到某一 channel 就緒
- 如果有
default
,則在 3 情形中不會等待,而是立即執行 default 中的程式碼
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go ping(ch1)
go pong(ch2)
go print(ch1, ch2)
var input string
fmt.Scanln(&input)
}
func ping(ch chan int) {
time.Sleep(2 * time.Second)
ch<-1
}
func pong(ch chan int) {
time.Sleep(3 * time.Second)
ch<-2
}
func print(ch1, ch2 chan int) {
select {
case msg := <-ch1:
fmt.Println(msg)
case msg := <-ch2:
fmt.Println(msg)
}
}
兩秒鐘之後,輸出:1
在 select 語句中新增下面程式碼:
default:
fmt.Println("nothing received.")
輸出: nothing received.
總結
Golang 將執行緒抽象出來成為輕量級的 goroutine,開發者不再需要過多地關注 OS 層面的邏輯,終於能夠從併發程式設計中解放出來。
channel 作為 goroutine 通訊的媒介,安全高效的實現了 goroutine 之間的通訊和共享記憶體。
用 Effetive go 中的一句話來總結 [4]:
> Do not communicate by sharing memory; instead, share memory by communicating.
Reference
[1] https://blog.golang.org/concurrency-is-not-parallelism
[2] http://blog.nindalf.com/how-goroutines-work/
[3] https://www.golang-book.com/books/intro/10
[4] https://golang.org/doc/effective_go.html
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- golang併發程式設計Golang程式設計
- Golang 併發程式設計Golang程式設計
- Golang併發程式設計基礎Golang程式設計
- Golang 併發程式設計實踐Golang程式設計
- Golang 併發程式設計中條件變數的理解與使用Golang程式設計變數
- Golang 併發程式設計(channel實現)Golang程式設計
- Golang併發程式設計——goroutine、channel、syncGolang程式設計
- 我所理解的 iOS 併發程式設計iOS程式設計
- Java併發程式設計,深入理解ReentrantLockJava程式設計ReentrantLock
- Golang併發程式設計中select簡單瞭解Golang程式設計
- Java併發程式設計——深入理解自旋鎖Java程式設計
- golang 併發程式設計之生產者消費者Golang程式設計
- 併發程式設計程式設計
- go 併發程式設計案例三 golang 中的物件導向程式設計Golang物件
- Golang併發程式設計程式通訊channel瞭解及簡單使用Golang程式設計
- Java併發程式設計——深入理解執行緒池Java程式設計執行緒
- java併發程式設計系列:java併發程式設計背景知識Java程式設計
- java 併發程式設計Java程式設計
- 併發程式設計—— LinkedTransferQueue程式設計
- 併發程式設計(ReentrantLock)程式設計ReentrantLock
- Go 併發程式設計Go程式設計
- Python併發程式設計Python程式設計
- 併發程式設計 synchronized程式設計synchronized
- 併發程式設計(四)程式設計
- 併發程式設計(二)程式設計
- Java併發程式設計Java程式設計
- 併發程式設計13程式設計
- Go 併發程式設計 - 併發安全(二)Go程式設計
- Golang併發程式設計優勢與核心goroutine及注意細節Golang程式設計
- Python併發程式設計之從效能角度來初探併發程式設計(一)Python程式設計
- Java併發程式設計 - 第十一章 Java併發程式設計實踐Java程式設計
- 【Java併發程式設計】一、為什麼需要學習併發程式設計?Java程式設計
- Java併發程式設計-鎖及併發容器Java程式設計
- 併發程式設計(二)——併發類容器ConcurrentMap程式設計
- 併發程式設計之:JUC併發控制工具程式設計
- Golang併發程式設計有緩衝通道和無緩衝通道(channel)Golang程式設計
- Java併發程式設計—ThreadLocalJava程式設計thread
- Java併發程式設計:synchronizedJava程式設計synchronized
- 併發程式設計前傳程式設計