[TOC]
go的強(qiáng)大并發(fā)
在go語言中梆奈,go routine是并發(fā)的基本單位,是操作系統(tǒng)中提到的用戶級線程侦锯,輕量線程液兽,它的執(zhí)行切換不會(huì)觸發(fā)操作系統(tǒng)內(nèi)核態(tài)的轉(zhuǎn)換。channel儒老,mutex蝴乔,是go中進(jìn)行協(xié)程間 協(xié)調(diào),通信驮樊,以及控制競爭訪問 的重要機(jī)制薇正,程序間的通信一般就兩種:共享內(nèi)存和消息傳遞
Share memory by communicating, don’t communicate by sharing memory.
通過通信共享內(nèi)存,而不是通過共享內(nèi)存而通信囚衔。
線程挖腰,線程的閱讀
鎖和信號量的閱讀
神奇的channel
channel可以寫入,可以從中讀出练湿,并且是并發(fā)安全的猴仑,即多個(gè)routine同時(shí)讀寫是不會(huì)出現(xiàn)問題的。相當(dāng)于用于同步和通信的有鎖隊(duì)列
CSP 是 Communicating Sequential Process 的簡稱肥哎,中文可以叫做通信順序進(jìn)程辽俗,是一種并發(fā)編程模型,由 Tony Hoare 于 1977 年提出贤姆。簡單來說榆苞,CSP 模型由并發(fā)執(zhí)行的實(shí)體(線程或者進(jìn)程)所組成,實(shí)體之間通過發(fā)送消息進(jìn)行通信霞捡,這里發(fā)送消息時(shí)使用的就是通道坐漏,或者叫 channel。CSP 模型的關(guān)鍵是關(guān)注 channel碧信,而不關(guān)注發(fā)送消息的實(shí)體赊琳。Go 語言實(shí)現(xiàn)了 CSP 部分理論,goroutine 對應(yīng) CSP 中并發(fā)執(zhí)行的實(shí)體砰碴,channel 也就對應(yīng)著 CSP 中的 channel躏筏。
- 無緩存的channel
- 有緩存的channel
無緩存的channel的讀寫如果沒有對應(yīng)的反操作被執(zhí)行,會(huì)被阻塞呈枉。
Notice
不可以理解為是size為1的有緩沖channel趁尼,因?yàn)?. size 為1 且buf沒滿時(shí)候的發(fā)送或接收操作埃碱,若沒有反操作,不會(huì)阻塞 2. 可見性規(guī)則酥泞。
那是否可以理解為size=0砚殿,因?yàn)閟ize為0。 它的操作芝囤,若沒有反操作似炎,會(huì)阻塞。
根據(jù) 《可見性規(guī)范描述》第四條:The kth receive on a channel with capacity C happens before the k+Cth send from that channel completes. 則當(dāng)buffered的size為0悯姊, 第k條接收操作先于第k條發(fā)送操作的完成羡藐。等同于規(guī)則三。
有緩存的channel在緩存沒空悯许,或者沒滿之前仆嗦,讀或者寫都不會(huì)阻塞。
Notice
不要在一個(gè)goroutine中對同一個(gè)channel執(zhí)行發(fā)送和接收操作
Make(chan)不帶箭頭時(shí)先壕,通道是沒有方向的欧啤,但當(dāng)它被賦予的靜態(tài)類型是有方向的時(shí)候:當(dāng)一個(gè)通道被賦予給一個(gè)靜態(tài)類型為<-chan
的變量時(shí),goroutine只能從中接收启上,又稱為接收通道邢隧;被賦予給了 chan <-
變量時(shí),goroutine只能向它發(fā)送值冈在,又稱為發(fā)送channel倒慧。
Make(chan)的時(shí)候可以帶方向。
對于channel中發(fā)送和接收出去的都會(huì)是復(fù)制的值包券,不是原值纫谅。
一個(gè)channel的變量運(yùn)行中可以有三種狀態(tài):
- nil
- 開啟著的
- 關(guān)閉了的
nil channel
對一個(gè)nil的channel讀寫會(huì)被阻塞,當(dāng)用在select
語句中時(shí)溅固,會(huì)被強(qiáng)制忽略付秕。當(dāng)for-select
中讀到被關(guān)閉的channel時(shí),即生產(chǎn)者侍郭,關(guān)閉了channel询吴,這樣在下次的循環(huán)中就不會(huì)從中讀取到值了,因?yàn)閷τ陉P(guān)閉了的channel亮元,總是讀到對應(yīng)類型的零值猛计。
for {
select {
case v, ok<- 被關(guān)閉了的channel的變量 variable:
if ok {dov} else { 將被關(guān)閉的channel的variable置為 nil}
case v <- 其他channel
}
}
開啟著的channel
對開啟的channel的讀寫,是否阻塞取決于是否還有空的緩存位爆捞。
關(guān)閉著的channel
- 從一個(gè)關(guān)閉的channel接收奉瘤,永遠(yuǎn) 不會(huì)阻塞,立刻會(huì)讀到channel內(nèi)類型的零值煮甥。
- 而對一個(gè)關(guān)閉了channel發(fā)送盗温,會(huì)panic藕赞。
- 并且重復(fù)關(guān)閉一個(gè)已經(jīng)關(guān)閉了的channel也會(huì)panic。
- 關(guān)閉一個(gè)沒有了緩存的channel(或者本身就是無緩存channel)卖局, 所有阻塞在 channel接收操作上的阻塞會(huì)立刻返回零值找默。
另外
close 內(nèi)置函數(shù)關(guān)閉一個(gè)通道,該通道必須是雙向的或僅發(fā)送的吼驶。
close(make(<-chan int, 10 )) 這個(gè)close會(huì)報(bào)錯(cuò), 因?yàn)槭莻€(gè)接收channel店煞, goroutine從中接收元素
channel 的關(guān)閉應(yīng)僅由發(fā)送方執(zhí)行蟹演,而不應(yīng)由接收方執(zhí)行,并且在收到最后發(fā)送的值后具有關(guān)閉通道的效果顷蟀。
即goroutine在沒有信息需要發(fā)送到channel的時(shí)候酒请,就關(guān)閉它
因?yàn)檫@幾個(gè)特點(diǎn),所以在channel必須被優(yōu)雅關(guān)閉鸣个,另外可以利用它們做一些特殊的操作羞反,如:
- 通過4,我們可以實(shí)現(xiàn)結(jié)束再通知
- 因?yàn)?囤萤,我們在
for-select
中必須做好判斷:是否關(guān)閉昼窗,然后如何避免再次讀到,是否可以置為nil
(有時(shí)候不能置為nil涛舍,這個(gè)時(shí)候貌似都可以用for-range
, 它的特殊在于會(huì)自己判斷close然后跳出來)
通道的關(guān)閉原則
關(guān)于channel的一些關(guān)閉的資料, 雖然我并不完全認(rèn)同澄惊,但是也有可取之處,比如:
利用生產(chǎn)者(goroutine)和消費(fèi)者(goroutine)的各類場景來闡述channel的正確用法
對關(guān)閉channel的執(zhí)著富雅,與細(xì)致入微的分析思考:一定要關(guān)閉
channel
以避免死鎖因?yàn)橄M(fèi)者的意外退出(一個(gè)存活的都沒了)而導(dǎo)致生產(chǎn)者的發(fā)送channel阻塞掸驱,而導(dǎo)致了死鎖,是死鎖嗎没佑?這不是goroutine泄露嗎毕贼?通過
defer
強(qiáng)行關(guān)閉來自生產(chǎn)者的發(fā)送channel(必須是發(fā)送或雙向的),然后讓生產(chǎn)者因?yàn)榈诙€(gè)特點(diǎn), 而panic蛤奢,再利用defer
處理這個(gè)異常鬼癣,是的生產(chǎn)者退出。消費(fèi)者(只有一個(gè))通過多加一個(gè)
channel
來通知生產(chǎn)者啤贩,主動(dòng)讓它停止生產(chǎn):向取消channel中發(fā)送一個(gè)值扣溺,或者關(guān)閉它,單個(gè)的生產(chǎn)者接收到一個(gè)值瓜晤,主動(dòng)關(guān)閉生產(chǎn)chan和取消chan锥余。多個(gè)生產(chǎn)者接收到關(guān)閉信號,主動(dòng)關(guān)閉生產(chǎn)痢掠。小問題: 生產(chǎn)者已經(jīng)生產(chǎn)的值驱犹,在chan中了的嘲恍,可以消費(fèi)亦可以不消費(fèi),看選擇了雄驹。上面的情況佃牛,如果消費(fèi)者有多個(gè)呢,同時(shí)要求停止生產(chǎn)医舆,即 n-m 的消費(fèi)與生產(chǎn)俘侠,有什么好的通用方法?每個(gè)都關(guān)閉取消chan蔬将,同時(shí)都要做
defer
的recover
還是上面的情況爷速,若是生產(chǎn)者已經(jīng)生產(chǎn)了的,雖然還沒放到chan中的霞怀,如果想繼續(xù)出來怎么辦惫东,那就在檢測在取消chan的關(guān)閉后,繼續(xù)發(fā)送(可以主動(dòng)置為nil或者在檢測關(guān)閉后發(fā)送)并且消費(fèi)側(cè)也要工作毙石。
如果消費(fèi)者廉沮,在消費(fèi)結(jié)束以后(即生產(chǎn)者關(guān)閉了),所有的消費(fèi)者消費(fèi)完了生產(chǎn)者的生產(chǎn)徐矩,可以用計(jì)數(shù)器柵欄
WaitGroup
來統(tǒng)計(jì)狀態(tài)(柵欄的+1決不能放在子協(xié)程內(nèi))
在設(shè)計(jì)上滞时,不管是消費(fèi)者與生產(chǎn)者: 1-1, 1-n滤灯,n-1漂洋,n-m,都要避免channel的重復(fù)關(guān)閉力喷,如果有一定要defer recover
刽漂。且總有一個(gè)routine
知道發(fā)送已經(jīng)完成了,或者都知道弟孟;總有一個(gè)routine知道接收結(jié)束了贝咙,或者都知道,即接收的channel被發(fā)送方關(guān)閉了拂募,接下里的操作是每個(gè)routine都做庭猩,還是統(tǒng)一一起做,用柵欄陈症。一定要注意蔼水,如果難以避免重復(fù)關(guān)閉,無法關(guān)閉录肯,那一定是設(shè)計(jì)上有問題趴腋,并且很可能routine泄露
, 殘留數(shù)據(jù)為發(fā)送,未處理,未接受等优炬。
疑問:
如果消費(fèi)者被退出颁井,生產(chǎn)者也停止,并且關(guān)閉了它們的channel
蠢护,但是這個(gè)channel的緩存中還有值雅宾,這個(gè)算什么(大概其實(shí)是上面第四點(diǎn))?泄露葵硕?會(huì)被回收嗎眉抬?
答:
一個(gè)channel,無論是否被關(guān)閉懈凹,只要沒有再被引用蜀变,就會(huì)被go語言的垃圾回收器自動(dòng)回收,即盧軒認(rèn)為:channel不需要通過close來確保被回收蘸劈,只有當(dāng)需要告訴channel的接收者,不會(huì)再有數(shù)據(jù)發(fā)送過去了尊沸,才會(huì)需要關(guān)閉channel威沫。
并發(fā)模式
流水線模型
模型來自現(xiàn)實(shí)世界的啟發(fā),是對現(xiàn)實(shí)世界的生產(chǎn)消費(fèi)洼专,通知發(fā)布等模式的抽象棒掠,模仿。
流水線由多個(gè)階段組成屁商,階段間用channel
鏈接烟很,每個(gè)階段可以由同時(shí)運(yùn)行的多個(gè)go routine
組成。這里也可以看出go并發(fā)的思想蜡镶,通過通信來共享數(shù)據(jù)雾袱,內(nèi)存,信息官还,控制等芹橡。
-
Fan-In: 扇入,一個(gè)
goroutine
從多個(gè)channel
讀取數(shù)據(jù)望伦,直到它們都關(guān)閉林说,In,代表一種收斂的模式屯伞,常用來做結(jié)果的收集腿箩。 -
Fan-Out:扇出,多個(gè)
goroutine
從一個(gè)channel
讀取數(shù)據(jù)劣摇,知道它關(guān)閉珠移,Out,代表一種發(fā)散的模式,常用來做任務(wù)的分發(fā)剑梳。
將Fan-In和Fan-out結(jié)合起來唆貌,可以實(shí)現(xiàn)豐富且強(qiáng)大的流水線模型。流水線模型的channel通常是有緩存的垢乙,這樣能搞適當(dāng)提高程序性能锨咙。
Sync包:mutex
channel的是go語言的主角,但是也有它不能完美解決的場景追逮,mutex
就可以彌補(bǔ)酪刀,且channel
的開銷其實(shí)是大于mutex
的,并且從結(jié)構(gòu)來看钮孵,channel
有鎖也有cond骂倘。
解決問題的對比
channel
DataFlow -> Drawing -> Pipieline -> Exiting
數(shù)據(jù)是流動(dòng)的,可以Drawing
將其畫出來, pipeline
就是流水線巴席, 然后退出历涝。所以它適合的點(diǎn)在于:數(shù)據(jù)的流動(dòng)
- 傳遞數(shù)據(jù)的所有權(quán),將數(shù)據(jù)發(fā)給其他協(xié)程
- 分發(fā)任務(wù)漾唉,任務(wù)就是數(shù)據(jù)
- 交流異步結(jié)果荧库,結(jié)果是數(shù)據(jù)
mutex
適合不動(dòng)(不移動(dòng))的數(shù)據(jù), 1. 緩存 2. 狀態(tài)
這兩個(gè)該如何理解赵刑?分衫??更新緩存般此?改狀態(tài)蚪战?
不同的goroutine之間不再像合作,而像是競爭铐懊。對臨界區(qū)資源的讀取或者修改邀桑。
sync.Once
= 原子操作配合互斥鎖 , 但是開銷比 原子操作 + 互斥鎖小科乎,atomic
包對基本類型和復(fù)雜對象都提供了原子操作的支持概漱。
Load
和Store
方法,分別來加載和保存數(shù)據(jù)喜喂,返回值都是interface{}
瓤摧,所以可以用于任何復(fù)雜自定義類型。所謂原子操作玉吁,就是無論任何適合讀照弥,都不會(huì)讀到一半的數(shù)據(jù),在jvm的并發(fā)中进副,使用不當(dāng)?shù)臅r(shí)候这揣,可以讀到初始化了一半的對象等悔常,是"破損的數(shù)據(jù)"。
var {
instance *Singleton // 某單例给赞, 非導(dǎo)出
once sync.Once
}
func Instance() *Singleton { // 導(dǎo)出机打,單例的獲取方法
once.Do(func() {instance = &singleton{}})
return instance
}
cond
mutex
Pool
Map
WaitGroup
RWMutex
channel,mutex片迅,waitGroup
channe和mutex并不是相互對立的關(guān)系残邀,而是互補(bǔ),在某些場景我們選擇 channnel or mutex
, 復(fù)雜場景是channel and mutex
柑蛇,
甚至再加上WaitGroup
等待柵欄芥挣,流動(dòng)的數(shù)據(jù)在被處理完后,在某個(gè)點(diǎn)不動(dòng)耻台,等待其他處理的結(jié)束空免,這是BSP。
可見性
由于現(xiàn)代計(jì)算機(jī)cpu的架構(gòu)設(shè)計(jì)和實(shí)現(xiàn)盆耽,多核運(yùn)算的線程或者程序總是會(huì)遇到可見性問題蹋砚,這個(gè)問題在go中也存在,雖然go用的是協(xié)程摄杂,對于可見性坝咐,官方文檔。另外根據(jù)規(guī)范匙姜,main函數(shù)的執(zhí)行不會(huì)等待其他routine的運(yùn)行結(jié)束畅厢。當(dāng)初始化的時(shí)候冯痢,會(huì)按照main包里導(dǎo)入的其他包順序不斷深入導(dǎo)入并且初始化氮昧,是個(gè)遞歸的過程。在main.main
函數(shù)執(zhí)行之前所有代碼都運(yùn)行在同一個(gè)Goroutine中浦楣,也是運(yùn)行在程序的主系統(tǒng)線程中袖肥。如果某個(gè)init
函數(shù)內(nèi)部用go關(guān)鍵字啟動(dòng)了新的Goroutine的話,新的Goroutine和main.main
函數(shù)是并發(fā)執(zhí)行的振劳。
中文閱讀材料:(https://juejin.cn/post/6911126210340716558#heading-7)
常見并發(fā)模式
在神奇的channel中椎组,有一些直觀的對并發(fā)模式的思考。
消費(fèi)者和生產(chǎn)者:生產(chǎn)者的生產(chǎn)發(fā)給了消費(fèi)者历恐,相互了解
發(fā)布訂閱模式:發(fā)布者不關(guān)心誰在訂閱寸癌,它只是發(fā)布到了通道,由中間人弱贼,將通道的消息送給訂閱者
并發(fā)度的控制
我們利用channel
來傳遞蒸苇,通信,也可以利用阻塞 + 緩存 來實(shí)現(xiàn)并發(fā)度的控制吮旅。緩存 + 阻塞 = 帶一定數(shù)據(jù)的可阻塞可恢復(fù)的信號量溪烤。
啟動(dòng)goroutine
之前或者之中,操作緩存channel,等待channel不能操作的時(shí)候檬嘀,goroutine
的創(chuàng)建或者創(chuàng)建好的運(yùn)行中會(huì)阻塞槽驶,再每個(gè)運(yùn)行結(jié)束的channel會(huì)反操作channel,然后執(zhí)行流會(huì)恢復(fù)鸳兽。
利用這個(gè)方法掂铐,1. 可以控制并發(fā)度 2. 可以檢測channel的緩存的空余來判斷程序的運(yùn)行狀態(tài),通過已使用和空的位置比例判斷并發(fā)率贸铜。