CSP 并發(fā)模型
CSP(Communicating Sequential Processes),是用于描述兩個(gè)獨(dú)立的并發(fā)實(shí)體通過共享 channel(管道)進(jìn)行通信的并發(fā)模型。
go 并發(fā)模型
Go 語言中有兩種并發(fā)編程模型,除了普遍認(rèn)知的多線程共享內(nèi)存模型,還把 CSP 的思想融入到語言的核心里,基于 goroutine 和 channel 實(shí)現(xiàn)了其特有的 CSP 并發(fā)模型,使并發(fā)編程成為 Go 的一個(gè)獨(dú)特優(yōu)勢(shì)。
goroutine 和 channel 是一對(duì)組合。goroutine 是執(zhí)行并發(fā)的實(shí)體,而每個(gè)實(shí)體之間通過 channel 通信來實(shí)現(xiàn)數(shù)據(jù)共享。
go 并發(fā)原則
Do not communicate by sharing memory; instead, share memory by communicating.
不要通過共享內(nèi)存來通信,而要通過通信來實(shí)現(xiàn)內(nèi)存共享。
即不推薦使用 sync 包里的 mutex 等組件,而是使用 channel 進(jìn)行并發(fā)編程。可以使用原子函數(shù)、互斥鎖等 ,但使用 channel 更優(yōu)雅。
但兩者其實(shí)都是必要且有效的。實(shí)際上 channel 的底層就是通過 mutex 來控制并發(fā)的,只是 channel 是更高一層次的并發(fā)編程原語,封裝了更多的功能。
是選擇 sync 包里的底層并發(fā)編程原語還是 channel,參考如下決策樹:
channel
什么是 channel
channel 是 goroutine 之間通信的管道。channel 是線程安全的,并提供“先進(jìn)先出”的特性。
使用
聲明和初始化
var c chan int // 聲明了一個(gè) nil 通道
c = make(chan int) // 初始化了一個(gè)無緩沖通道,其值是一個(gè)地址,類型是 chan int
c = make(chan int, 100) // 初始化了一個(gè)緩沖區(qū)大小為100的通道
發(fā)送和接收
go func() {c <- 100}() // 發(fā)送數(shù)據(jù)到通道中
i := <-c // 從通道中接收數(shù)據(jù)
實(shí)現(xiàn)原理
數(shù)據(jù)結(jié)構(gòu)
type hchan struct {
// chan 里元素?cái)?shù)量
qcount uint
// chan 底層循環(huán)數(shù)組的長(zhǎng)度
dataqsiz uint
// 指向底層循環(huán)數(shù)組的指針,只針對(duì)有緩沖的 channel
buf unsafe.Pointer
// chan 中元素大小
elemsize uint16
// chan 是否被關(guān)閉的標(biāo)志
closed uint32
// chan 中元素類型
elemtype *_type // element type
// 已發(fā)送元素在循環(huán)數(shù)組中的索引
sendx uint // send index
// 已接收元素在循環(huán)數(shù)組中的索引
recvx uint // receive index
// 等待接收的 goroutine 隊(duì)列
recvq waitq // list of recv waiters
// 等待發(fā)送的 goroutine 隊(duì)列
sendq waitq // list of send waiters
// 保護(hù) hchan 中所有字段
lock mutex
}
buf
指向底層循環(huán)數(shù)組,只有緩沖型的 channel 才有。
sendx
, recvx
均指向底層循環(huán)數(shù)組,表示當(dāng)前可以發(fā)送和接收的元素位置索引值(相對(duì)于底層數(shù)組)。
sendq
, recvq
分別表示被阻塞的 goroutine,這些 goroutine 由于嘗試讀取 channel 或向 channel 發(fā)送數(shù)據(jù)而被阻塞。
waitq
是 sudog
的一個(gè)雙向鏈表,而 sudog
實(shí)際上是對(duì) goroutine 的一個(gè)封裝:
type waitq struct {
first *sudog
last *sudog
}
lock
用來保證每個(gè)讀 channel 或?qū)?channel 的操作都是原子的。
例如,創(chuàng)建一個(gè)容量為 6 的,元素為 int 型的 channel 數(shù)據(jù)結(jié)構(gòu)如下 :
創(chuàng)建
當(dāng)使用 make 函數(shù)創(chuàng)建通道時(shí),底層創(chuàng)建函數(shù)如下:
func makechan(t *chantype, size int64) *hchan
從函數(shù)原型來看,創(chuàng)建的 chan 是一個(gè)指針,和 map 相似。
接收和發(fā)送
對(duì) channel 的發(fā)送和接收操作都會(huì)在編譯期間轉(zhuǎn)換成為底層的發(fā)送接收函數(shù)。
channel 的發(fā)送和接收操作本質(zhì)上都是 “值的拷貝”。
緩沖通道
對(duì)于無緩沖通道,讀寫通道會(huì)立馬阻塞當(dāng)前協(xié)程。一個(gè)協(xié)程被通道操作阻塞后,Go 調(diào)度器會(huì)去調(diào)用其他可用的協(xié)程,這樣程序就不會(huì)一直阻塞。
而對(duì)于緩沖通道,寫不會(huì)阻塞當(dāng)前通道,直到通道滿了,同理,讀操作也不會(huì)阻塞當(dāng)前通道,除非通道沒數(shù)據(jù)。
也可以說無緩沖的 channel 是同步的,而有緩沖的 channel 是異步的。
創(chuàng)建一個(gè)緩沖通道:
ch := make(chan type, capacity)
capacity 是緩沖大小,必須大于 0。內(nèi)置函數(shù) len()、cap() 可以計(jì)算通道的長(zhǎng)度和容量。
如果緩沖通道是關(guān)閉狀態(tài)但有數(shù)據(jù),仍然可以讀取數(shù)據(jù)。
單向通道
主要用在通道作為參數(shù)傳遞的時(shí)候,Go 提供了自動(dòng)轉(zhuǎn)化,雙向轉(zhuǎn)單向。
使用單向通道主要是可以提高程序的類型安全性,程序不容易出錯(cuò)。
關(guān)閉通道
使用內(nèi)置函數(shù)close(ch)
可以關(guān)閉通道。
判斷通道關(guān)閉狀態(tài)
-
數(shù)據(jù)接收方可以通過返回狀態(tài)判斷通道是否已關(guān)閉:
val, ok := <- ch
ok 用于判斷 ch 是否已關(guān)閉且沒有緩沖值可以讀取。為 true,該通道還可以進(jìn)行讀寫操作;為 false,通道已關(guān)閉且沒有緩沖數(shù)據(jù),不能再進(jìn)行數(shù)據(jù)傳輸,返回的對(duì)應(yīng)類型的零值。
-
for range 讀取通道,通道關(guān)閉,for range 自動(dòng)退出
for v := range ch { fmt.Println(v) }
應(yīng)注意,使用 for range 讀取一個(gè)通道,數(shù)據(jù)寫入完畢后必須關(guān)閉通道,否則協(xié)程阻塞。
如何優(yōu)雅地關(guān)閉 channel
關(guān)于 channel 的使用,有幾點(diǎn)不方便的地方:
- 在不改變 channel 自身狀態(tài)的情況下,無法獲知一個(gè) channel 是否關(guān)閉。
- 關(guān)閉一個(gè) closed channel 會(huì)導(dǎo)致 panic。所以,如果關(guān)閉 channel 的一方在不知道 channel 是否處于關(guān)閉狀態(tài)時(shí)就去貿(mào)然關(guān)閉 channel 是很危險(xiǎn)的事情。
- 向一個(gè) closed channel 發(fā)送數(shù)據(jù)會(huì)導(dǎo)致 panic。所以,如果向 channel 發(fā)送數(shù)據(jù)的一方不知道 channel 是否處于關(guān)閉狀態(tài)時(shí)就去貿(mào)然向 channel 發(fā)送數(shù)據(jù)是很危險(xiǎn)的事情。
應(yīng)該如何優(yōu)雅地關(guān)閉 channel?
根據(jù) sender 和 receiver 的個(gè)數(shù),分下面幾種情況:
- 一個(gè) sender,一個(gè) receiver
- 一個(gè) sender, M 個(gè) receiver
- N 個(gè) sender,一個(gè) reciver
- N 個(gè) sender, M 個(gè) receiver
對(duì)于 1,2,只有一個(gè) sender 的情況就不用說了,直接從 sender 端關(guān)閉就好了。
第 3 種情形下,優(yōu)雅關(guān)閉 channel 的方法是增加一個(gè)傳遞關(guān)閉信號(hào)的 channel,receiver 通過信號(hào) channel 下達(dá)關(guān)閉數(shù)據(jù) channel 的命令。senders 監(jiān)聽到關(guān)閉信號(hào)后,停止發(fā)送數(shù)據(jù)。
func main() {
rand.Seed(time.Now().UnixNano())
const Max = 100000
const NumSenders = 1000
dataCh := make(chan int, 100)
stopCh := make(chan struct{})
var wg sync.WaitGroup
wg.Add(NumSenders)
for i := 0; i < NumSenders; i++ {
go func() {
for {
select {
case <-stopCh:
wg.Done()
return
case dataCh <- rand.Intn(Max):
}
}
}()
}
go func() {
for value := range dataCh {
if value == Max-1 {
fmt.Println("send stop signal to senders.")
close(stopCh)
return
}
fmt.Println(value)
}
}()
wg.Wait()
fmt.Println("main exit")
}
stopCh 就是信號(hào) channel,receiver 關(guān)閉 stopCh 來通知 senders 停止發(fā)送數(shù)據(jù)。上面的代碼沒有明確關(guān)閉 dataCh,在 Go 語言中,對(duì)于一個(gè) channel,如果最終沒有任何 goroutine 引用它,不管 channel 有沒有被關(guān)閉,最終都會(huì)被 gc 回收。所以,在這種情形下,所謂的優(yōu)雅地關(guān)閉 channel 就是不關(guān)閉 channel,讓 gc 代勞。
第四種情況和第三種情況不同,這里有 M 個(gè) receiver,如果還是采用第三種方案,由 receiver 直接關(guān)閉 stopCh 的話,就會(huì)重復(fù)關(guān)閉一個(gè) channel,導(dǎo)致 panic。因此需要增加一個(gè)中間人,M 個(gè) receiver 都向他發(fā)送關(guān)閉 dataCh 的請(qǐng)求,中間人收到第一個(gè)請(qǐng)求后下達(dá)關(guān)閉 dataCh 的指令(關(guān)閉 stopCh)。當(dāng)然,這里的 N 個(gè) sender 也可以向中間人發(fā)送關(guān)閉 dataCh 的請(qǐng)求。
func main() {
rand.Seed(time.Now().UnixNano())
const Max = 100000
const NumSenders = 1000
const NumReceivers = 10
dataCh := make(chan int, 100)
stopCh := make(chan struct{})
toStop := make(chan string, NumSenders + NumReceivers)
exitCh := make(chan struct{})
go func() {
s := <-toStop
fmt.Println("toStop s:", s)
close(stopCh)
exitCh <- struct{}{}
}()
for i := 0; i < NumSenders; i++ {
go func(id string) {
for {
value := rand.Intn(Max)
if value == 0 {
toStop <- "sender#" + id
return
}
select {
case <-stopCh:
return
case dataCh <- value:
}
}
}(strconv.Itoa(i))
}
for i := 0; i < NumReceivers; i++ {
go func(id string) {
for {
select {
case <- stopCh:
return
case value := <-dataCh:
if value == Max-1 {
toStop <- "receiver#" + id
return
}
fmt.Println(value)
}
}
}(strconv.Itoa(i))
}
select {
case <-exitCh:
time.Sleep(time.Second)
}
fmt.Println("main exit")
}
代碼里 toStop 就是中間人的角色,使用它來接收 senders 和 receivers 發(fā)送過來的關(guān)閉 dataCh 請(qǐng)求。這里同樣沒有真正關(guān)閉 dataCh,讓 gc 代勞。
常見異常操作
操作 nil 或 closed channel
close 關(guān)閉一個(gè) nil channel,會(huì)引起 panic。
close 關(guān)閉一個(gè) closed channel,會(huì)引起 panic。
往一個(gè) nil channel 發(fā)送數(shù)據(jù),會(huì)造成阻塞。
從一個(gè) nil channel 接收數(shù)據(jù),會(huì)造成阻塞。
往一個(gè) closed channel 發(fā)送數(shù)據(jù),會(huì)引起 panic。
從一個(gè) closed channel 接收數(shù)據(jù),返回已緩沖數(shù)據(jù)或者零值。
阻塞、死鎖
- 無緩沖通道寫或者讀數(shù)據(jù),當(dāng)前協(xié)程阻塞。
- 有緩沖通道已滿,再往通道中寫數(shù)據(jù),當(dāng)前協(xié)程阻塞。
- 使用 for range 讀取一個(gè)通道,數(shù)據(jù)寫入端寫入完畢后必須關(guān)閉通道,否則 for range 語句所在協(xié)程阻塞。
- 空 select 語句
select {}
,沒有 case 分支,當(dāng)前協(xié)程阻塞。 - 當(dāng)前協(xié)程阻塞又沒有其他可用協(xié)程時(shí),死鎖。
內(nèi)存泄漏
- channel 可能會(huì)引發(fā) goroutine 泄漏,原因是 goroutine 操作 channel 后,處于發(fā)送或接收阻塞狀態(tài),而 channel 處于滿或空的狀態(tài),一直得不到改變。同時(shí),垃圾回收器也不會(huì)回收此類資源,進(jìn)而導(dǎo)致 goroutine 一直處于等待隊(duì)列中。
通道應(yīng)用
停止信號(hào)
channel 多用于停止信號(hào)的場(chǎng)景,關(guān)閉 channel 或者向 channel 發(fā)送一個(gè)元素,使得接收 channel 的那一方獲知此信息,進(jìn)而做后續(xù)操作。
任務(wù)定時(shí)
與 timer 結(jié)合,實(shí)現(xiàn)超時(shí)控制。
// 等待 1 分鐘后,如果 dataCh 還沒有讀出數(shù)據(jù)或者被關(guān)閉,就直接結(jié)束。
select {
case <-time.After(time.Minute):
case <-dataCh:
fmt.Println("do something")
}
或定期執(zhí)行任務(wù)。
// 每隔一秒執(zhí)行任務(wù)
ticker := time.Tick(time.Second)
for {
select {
case <- ticker:
fmt.Println("do something")
}
}
解耦生產(chǎn)方和消費(fèi)方
生產(chǎn)方往 taskCh 塞任務(wù),消費(fèi)方循環(huán)從 channel 中拿任務(wù),解耦。
控制并發(fā)數(shù)
某些場(chǎng)景因?yàn)橘Y源限制等原因,需要控制并發(fā)數(shù)量。
limit := make(chan int, 3)
for _, w := range work {
go func() {
limit <- 1
w()
<-limit
}()
}
真正執(zhí)行任務(wù),訪問第三方的動(dòng)作在 w() 中完成,在執(zhí)行 w() 之前,先要從 limit 中拿“許可證”,拿到許可證之后,才能執(zhí)行 w(),并且在執(zhí)行完任務(wù),要將“許可證”歸還。這樣就可以控制同時(shí)運(yùn)行的 goroutine 數(shù)。
還有一點(diǎn)要注意的是,如果 w() 發(fā)生 panic,那“許可證”可能就還不回去了,因此需要使用 defer 來保證。