如何實(shí)現(xiàn)多線程之間的通信形娇,是并發(fā)模型里面最需要被考慮到的問題版姑。golang為此引進(jìn)了channel豁生,channel可作為goroutine之間交流的通道半等。每一個(gè)channel都可讀可寫屡立,且都是阻塞的洒忧,也即受神,當(dāng)一個(gè)goroutine讀一個(gè)channel的時(shí)候俺猿,就被阻塞住了碘举,直到另一個(gè)goroutine向這個(gè)channel寫入信息忘瓦。這個(gè)特性也常被用于synchronization。
聲明一個(gè)channel ,類型為int引颈。
c := make(chan int)
向channel寫入值
c <- 1
讀取channel的值并用于初始化a
a := <- c
另外有帶緩沖的channel:
buff := make(chan int, 10)
向buff寫入數(shù)據(jù)不會(huì)阻塞耕皮,但當(dāng)寫滿10個(gè)時(shí),就會(huì)被阻塞住蝙场,直到buff的數(shù)據(jù)被讀取明场,可視為一個(gè)帶長度限制的隊(duì)列。
channel的用處簡單明了李丰,任何需要線程之間交換傳遞數(shù)據(jù)的地方都可以用到channel苦锨。下面一個(gè)簡單的例子,最能說明channel的簡單和強(qiáng)大。
想象一個(gè)場景舟舒,若干只老鼠依次排開拉庶,最右邊的老鼠向它左邊的老鼠說一句話,左邊的老鼠聽到后秃励,又傳給它左邊的老鼠氏仗,直到最左邊的老鼠知道了這句話。這個(gè)過程可以用下面的代碼描述夺鲜。函數(shù)gopher代表一只老鼠皆尔,負(fù)責(zé)將右邊聽到的信息傳給左邊。在main函數(shù)的循環(huán)里面币励,定了次數(shù)1000000慷蠕,也就是有一百萬只老鼠參加了這個(gè)游戲,每次循環(huán)都make出新的channel食呻,最后流炕,向最右邊的channel寫入’i am hungry’(也就是left,因?yàn)樽詈髄eft = right)仅胞,并打印出最左邊(mostleft)收到的信息每辟。那么,完成整個(gè)過程需要多久呢干旧?在我的虛擬機(jī)里面(4g內(nèi)存渠欺,單核),一百萬只老鼠花費(fèi)的時(shí)間是12秒椎眯,下面的代碼編譯后直接可運(yùn)行的挠将,讀者可以試著調(diào)下循環(huán)的次數(shù)并觀察goroutine的增加對(duì)運(yùn)行時(shí)間和機(jī)器的影響。
package main
import (
"time"
"fmt"
)
func main() {
tbeg := time.Now()
mostleft := make(chan string)
left := mostleft
for i := 0;i < 1000000;i++ {
right := make(chan string)
go gopher(left, right)
left = right
}
left <- "i am hungry"
fmt.Println(<- mostleft)
cost := time.Now().Sub(tbeg)
fmt.Println(“cost: “, cost)
}
func gopher(left, right chan string) {
left <- <- right //所有的gopher均會(huì)被阻塞住直到最右邊收到消息
}
由上面的例子可以看出盅视,channel非常適合消息傳遞的場合,然而旦万,golang被廣為流傳的有一句話說到:Do not communicate by sharing memory; instead, share memory by communicating.所以闹击,channel應(yīng)當(dāng)替代Mutex?成艘?赏半?
我認(rèn)為,這句話最多只能算做golang宣傳的口號(hào)淆两,并不能當(dāng)成實(shí)踐真理断箫。并發(fā)模型的通信機(jī)制無非兩種,共享內(nèi)存和消息傳遞秋冰。channel只是作為消息傳遞的一種實(shí)現(xiàn)仲义,并不能說它就比共享內(nèi)存的做法更先進(jìn)或者簡潔。channel更合適數(shù)據(jù)傳遞收發(fā)的場景,mutex則適合共享數(shù)據(jù)讀取的場景埃撵。
func (t *Worker)loop(c chan string) {
for {
select {
case s := <- c:
t.doSomething(s)
case <- t.stop:
break
}
}
}
上面的例子赵颅,守護(hù)函數(shù)從channel s 或者channel t.stop里面獲取消息,select語句用于從多個(gè)channel里面選取其一暂刘,當(dāng)某個(gè)channel準(zhǔn)備好的時(shí)候饺谬,就跳到那個(gè)對(duì)應(yīng)的case。loop函數(shù)傾聽著兩個(gè)數(shù)據(jù)來源谣拣,收到數(shù)據(jù)后進(jìn)行處理募寨,當(dāng)t.stop收到消息時(shí),則退出森缠。這種數(shù)據(jù)傳遞收發(fā)的場景拔鹰,用起channel來就簡潔明了,如果是Mutex的話辅鲸,則要不斷加鎖->判斷數(shù)據(jù)是否準(zhǔn)備好->解鎖->sleep等待格郁,很是麻煩。
而在另外的場景中独悴,則用Mutex最好不過了
// goroutine 1
func update() {
lock.Lock()
html = XXX
lock.Unlock()
}
// goroutine 2
func handler(r Request, w writer) {
lock.Lock()
w.write(html)
lock.Unlock()
}
handler是一個(gè)網(wǎng)頁訪問的處理接口例书,當(dāng)收到一個(gè)請(qǐng)求的時(shí)候,負(fù)責(zé)返回html刻炒,而html的內(nèi)容會(huì)時(shí)而更新决采,由update函數(shù)進(jìn)行處理。這種場景下坟奥,用鎖是再好不過了树瞭,這時(shí)候非要share memory by communicating的話也不是不可以,只是會(huì)平添復(fù)雜的同步邏輯爱谁,讀者不妨嘗試一下晒喷,嘻嘻。
講道理访敌,golang對(duì)channel和mutex都支持得很好凉敲,所以無謂去爭論哪個(gè)更好,哪種用起來簡潔就用哪種寺旺,沒必要拘束于其中之一爷抓。畢竟白帽黑貓,能最快抓到老鼠的就是更好的貓阻塑。
前些日子蓝撇,一個(gè)沒注意在項(xiàng)目里弄了一個(gè)bug,自己檢查檢查不出來陈莽,最后才知道該加鎖的地方忘記加鎖了渤昌。Don’t be clever虽抄,是The Go Memory Model里給的忠告。下面這段代碼就是引發(fā)bug的地方耘沼,在這里列出代碼邏輯极颓,既說明下golang語法上一些特性,畢竟talk is cheap群嗤,show me the code : )菠隆。也借此尋求下讀者的意見,是否有更好的方法來重構(gòu)這段代碼狂秘,交流交流骇径。
// 需求:有規(guī)則集合R,數(shù)據(jù)庫D者春,每一條規(guī)則r需到D拿數(shù)據(jù)破衔,并判斷此條規(guī)則是否已經(jīng)符合。
// 要求:因?yàn)镽量比較大钱烟,且經(jīng)常變化晰筛,需要程序作為daemon循環(huán)的跑,實(shí)時(shí)性要求比較高拴袭,所以每跑一次耗費(fèi)的時(shí)間不能太長读第。
// 最簡單的處理辦法就是,從R一條條取出規(guī)則拥刻,然后一條條到D拿數(shù)據(jù)比對(duì)怜瞒,邏輯非常簡單,
// 但是般哼,這樣吴汪,每一萬條規(guī)則耗費(fèi)的時(shí)間 > 10 min。不符合要求蒸眠。
// 所以需要將規(guī)則聚合漾橙,將相似規(guī)則聚合成一條查詢sql到D取數(shù)據(jù),然后返回楞卡∷耍可是聚合怎么聚合呢?
// 每一條規(guī)則都有很多屬性臀晃,如果在主邏輯里面進(jìn)行聚合觉渴,將會(huì)使代碼不清晰介劫,且若以后增加規(guī)則屬性徽惋,
// 整個(gè)規(guī)則分類邏輯都要改。所以最好主邏輯還是一條條拿規(guī)則座韵,一條條取數(shù)據(jù)進(jìn)行判定险绘,這樣代碼會(huì)清晰很多踢京。
// 在這種方法下,每一萬條處理時(shí)間 < 4s
//被循環(huán)調(diào)用的函數(shù)宦棺,主邏輯
func Work() {
result := []Result{}
wait := sync.WaitGroup{}
for r := range R {
wait.Add(1)
//異步IO瓣距,輸入一條規(guī)則,返回對(duì)應(yīng)的數(shù)據(jù)代咸,然后在callback里面進(jìn)行判斷是否規(guī)則已符合
//NodeJS借鑒來的其實(shí)蹈丸,想一條進(jìn),一條出呐芥,而又想按規(guī)則聚合到數(shù)據(jù)庫拿數(shù)據(jù)逻杖,只想到這種方法了。
Select(r,func(r Rule, d Data){
result = append(result, r.Judge(d)) //這里沒加鎖會(huì)導(dǎo)致race conditions
wait.Done()
})
}
Do()
wait.Wait() //等待所有callback都被執(zhí)行了
doSomething(result)
}
//Select的實(shí)現(xiàn)封裝思瘟,這里可以一條條處理荸百,也可以聚合后再處理,已經(jīng)對(duì)外隱藏了滨攻。
func Select(r Rule, f Callback) {
count++
Class.add(r) //按規(guī)則屬性組合哈希值進(jìn)行聚合够话。這里再怎么復(fù)雜都沒關(guān)系了。
go func() {
<- done //必須等待Do()函數(shù)被執(zhí)行光绕,這樣getData()才能拿到數(shù)據(jù)女嘲。
f(r, getData(r))
}
}
func Do() {
//do dirty work
//按聚合的規(guī)則到D拿數(shù)據(jù)
...
...
//通知所有Select調(diào)用執(zhí)行callback
for i := 0;i < count;i++ {
done <- true
}
}
原文轉(zhuǎn)自謝培陽的博客