
如何實(shí)現(xiàn)多線程之間的通信,是并發(fā)模型里面最需要被考慮到的問題。golang為此引進(jìn)了channel,channel可作為goroutine之間交流的通道。每一個channel都可讀可寫,且都是阻塞的,也即,當(dāng)一個goroutine讀一個channel的時候,就被阻塞住了,直到另一個goroutine向這個channel寫入信息。這個特性也常被用于synchronization。
聲明一個channel ,類型為int。
c := make(chan int)
向channel寫入值
c <- 1
讀取channel的值并用于初始化a
a := <- c
另外有帶緩沖的channel:
buff := make(chan int, 10)
向buff寫入數(shù)據(jù)不會阻塞,但當(dāng)寫滿10個時,就會被阻塞住,直到buff的數(shù)據(jù)被讀取,可視為一個帶長度限制的隊(duì)列。
channel的用處簡單明了,任何需要線程之間交換傳遞數(shù)據(jù)的地方都可以用到channel。下面一個簡單的例子,最能說明channel的簡單和強(qiáng)大。
想象一個場景,若干只老鼠依次排開,最右邊的老鼠向它左邊的老鼠說一句話,左邊的老鼠聽到后,又傳給它左邊的老鼠,直到最左邊的老鼠知道了這句話。這個過程可以用下面的代碼描述。函數(shù)gopher代表一只老鼠,負(fù)責(zé)將右邊聽到的信息傳給左邊。在main函數(shù)的循環(huán)里面,定了次數(shù)1000000,也就是有一百萬只老鼠參加了這個游戲,每次循環(huán)都make出新的channel,最后,向最右邊的channel寫入’i am hungry’(也就是left,因?yàn)樽詈髄eft = right),并打印出最左邊(mostleft)收到的信息。那么,完成整個過程需要多久呢?在我的虛擬機(jī)里面(4g內(nèi)存,單核),一百萬只老鼠花費(fèi)的時間是12秒,下面的代碼編譯后直接可運(yùn)行的,讀者可以試著調(diào)下循環(huán)的次數(shù)并觀察goroutine的增加對運(yùn)行時間和機(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均會被阻塞住直到最右邊收到消息
}
由上面的例子可以看出,channel非常適合消息傳遞的場合,然而,golang被廣為流傳的有一句話說到:Do not communicate by sharing memory; instead, share memory by communicating.所以,channel應(yīng)當(dāng)替代Mutex???
我認(rèn)為,這句話最多只能算做golang宣傳的口號,并不能當(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語句用于從多個channel里面選取其一,當(dāng)某個channel準(zhǔn)備好的時候,就跳到那個對應(yīng)的case。loop函數(shù)傾聽著兩個數(shù)據(jù)來源,收到數(shù)據(jù)后進(jìn)行處理,當(dāng)t.stop收到消息時,則退出。這種數(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是一個網(wǎng)頁訪問的處理接口,當(dāng)收到一個請求的時候,負(fù)責(zé)返回html,而html的內(nèi)容會時而更新,由update函數(shù)進(jìn)行處理。這種場景下,用鎖是再好不過了,這時候非要share memory by communicating的話也不是不可以,只是會平添復(fù)雜的同步邏輯,讀者不妨嘗試一下,嘻嘻。
講道理,golang對channel和mutex都支持得很好,所以無謂去爭論哪個更好,哪種用起來簡潔就用哪種,沒必要拘束于其中之一。畢竟白帽黑貓,能最快抓到老鼠的就是更好的貓。
前些日子,一個沒注意在項(xiàng)目里弄了一個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í)時性要求比較高,所以每跑一次耗費(fèi)的時間不能太長。
// 最簡單的處理辦法就是,從R一條條取出規(guī)則,然后一條條到D拿數(shù)據(jù)比對,邏輯非常簡單,
// 但是,這樣,每一萬條規(guī)則耗費(fèi)的時間 > 10 min。不符合要求。
// 所以需要將規(guī)則聚合,將相似規(guī)則聚合成一條查詢sql到D取數(shù)據(jù),然后返回??墒蔷酆显趺淳酆夏??
// 每一條規(guī)則都有很多屬性,如果在主邏輯里面進(jìn)行聚合,將會使代碼不清晰,且若以后增加規(guī)則屬性,
// 整個規(guī)則分類邏輯都要改。所以最好主邏輯還是一條條拿規(guī)則,一條條取數(shù)據(jù)進(jìn)行判定,這樣代碼會清晰很多。
// 在這種方法下,每一萬條處理時間 < 4s
//被循環(huán)調(diào)用的函數(shù),主邏輯
func Work() {
result := []Result{}
wait := sync.WaitGroup{}
for r := range R {
wait.Add(1)
//異步IO,輸入一條規(guī)則,返回對應(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)) //這里沒加鎖會導(dǎo)致race conditions
wait.Done()
})
}
Do()
wait.Wait() //等待所有callback都被執(zhí)行了
doSomething(result)
}
//Select的實(shí)現(xiàn)封裝,這里可以一條條處理,也可以聚合后再處理,已經(jīng)對外隱藏了。
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)自謝培陽的博客