Golang系列之Synchronization (四)

如何實(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)自謝培陽的博客

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 能力模型 選擇題 [primary] 下面屬于關(guān)鍵字的是()A. funcB. defC. structD. cl...
    _張曉龍_閱讀 25,121評論 14 224
  • 控制并發(fā)有三種種經(jīng)典的方式,一種是通過channel通知實(shí)現(xiàn)并發(fā)控制 一種是WaitGroup,另外一種就是Con...
    wiseAaron閱讀 10,826評論 4 34
  • Goroutines 模型:和其他goroutine在共享的地址空間中并發(fā)執(zhí)行的函數(shù) 資源消耗: 初始時非常小的棧...
    大漠狼道閱讀 1,344評論 0 8
  • 今天介紹一下 go語言的并發(fā)機(jī)制以及它所使用的CSP并發(fā)模型 CSP并發(fā)模型 CSP模型是上個世紀(jì)七十年代提出的,...
    falm閱讀 68,816評論 10 80
  • go語言的并發(fā)機(jī)制以及它所使用的CSP并發(fā)模型 CSP并發(fā)模型CSP模型是上個世紀(jì)七十年代提出的,用于描述兩個獨(dú)立...
    seven_son閱讀 2,644評論 0 5

友情鏈接更多精彩內(nèi)容