web編程

4.4 併發通訊
從上面的例子中能夠看到,關鍵字go的引入使得在Go語言中併發編程變得簡單而優雅,但
咱們同時也應該意識到併發編程的原生複雜性,並時刻對併發中容易出現的問題保持警戒。別忘
了,咱們的例子還不能正常工做呢。
事實上,不論是什麼平臺,什麼編程語言,無論在哪,併發都是一個大話題。話題大小一般
也直接對應於問題的大小。併發編程的難度在於協調,而協調就要經過交流。從這個角度看來,

併發單元間的通訊是最大的問題。
在工程上,有兩種最多見的併發通訊模型:共享數據和消息。
共享數據是指多個併發單元分別保持對同一個數據的引用,實現對該數據的共享。被共享的
數據可能有多種形式,好比內存數據塊、磁盤文件、網絡數據等。在實際工程應用中最多見的無
疑是內存了,也就是常說的共享內存。
先看看咱們在C語言中一般是怎麼處理線程間數據共享的,如代碼清單4-2所示。
在上面的例子中,咱們在10個goroutine中共享了變量counter。每一個goroutine執行完成後,
將counter的值加1。由於10個goroutine是併發執行的,因此咱們還引入了鎖,也就是代碼中的
lock變量。每次對n的操做,都要先將鎖鎖住,操做完成後,再將鎖打開。在主函數中,使用for
循環來不斷檢查counter的值(一樣須要加鎖)。當其值達到10時,說明全部goroutine都執行完
畢了,這時主函數返回,程序退出。
事情好像開始變得糟糕了。實現一個如此簡單的功能,卻寫出如此臃腫並且難以理解的代碼。
想象一下,在一個大的系統中具備無數的鎖、無數的共享變量、無數的業務邏輯與錯誤處理分
支,那將是一場噩夢。這噩夢就是衆多C/C++開發者正在經歷的,其實Java和C#開發者也好不到
哪裏去。
Go語言既然以併發編程做爲語言的最核心優點,固然不至於將這樣的問題用這麼無奈的方
式來解決。Go語言提供的是另外一種通訊模型,即以消息機制而非共享內存做爲通訊方式。
消息機制認爲每一個併發單元是自包含的、獨立的個體,而且都有本身的變量,但在不一樣併發
單元間這些變量不共享。每一個併發單元的輸入和輸出只有一種,那就是消息。這有點相似於進程
的概念,每一個進程不會被其餘進程打擾,它只作好本身的工做就能夠了。不一樣進程間靠消息來通
信,它們不會共享內存。
Go語言提供的消息通訊機制被稱爲channel,接下來咱們將詳細介紹channel。如今,讓咱們
用Go語言社區的那句著名的口號來結束這一小節:
「不要經過共享內存來通訊,而應該經過通訊來共享內存。」
4.5 channel
channel是Go語言在語言級別提供的goroutine間的通訊方式。咱們可使用channel在兩個或
多個goroutine之間傳遞消息。channel是進程內的通訊方式,所以經過channel傳遞對象的過程和調
用函數時的參數傳遞行爲比較一致,好比也能夠傳遞指針等。若是須要跨進程通訊,咱們建議用
分佈式系統的方法來解決,好比使用Socket或者HTTP等通訊協議。Go語言對於網絡方面也有非
常完善的支持。
channel是類型相關的。也就是說,一個channel只能傳遞一種類型的值,這個類型須要在聲
明channel時指定。若是對Unix管道有所瞭解的話,就不難理解channel,能夠將其認爲是一種類
型安全的管道。
在瞭解channel的語法前,咱們先看下用channel的方式重寫上面的例子是什麼樣子的,以此
對channel先有一個直感的認識,如代碼清單4-4所示。
在這個例子中,咱們定義了一個包含10個channel的數組(名爲chs),並把數組中的每一個
channel分配給10個不一樣的goroutine。在每一個goroutine的Add()函數完成後,咱們經過ch <- 1語
句向對應的channel中寫入一個數據。在這個channel被讀取前,這個操做是阻塞的。在全部的
goroutine啓動完成後,咱們經過<-ch語句從10個channel中依次讀取數據。在對應的channel寫入
數據前,這個操做也是阻塞的。這樣,咱們就用channel實現了相似鎖的功能,進而保證了全部
goroutine完成後主函數才返回。是否是比共享內存的方式更簡單、優雅呢?
咱們在使用Go語言開發時,常常會遇到須要實現條件等待的場景,這也是channel能夠發揮
做用的地方。對channel的熟練使用,才能真正理解和掌握Go語言併發編程。下面咱們學習下
channel的基本語法
4.5.1 基本語法
通常channel的聲明形式爲:
var chanName chan ElementType
與通常的變量聲明不一樣的地方僅僅是在類型以前加了chan關鍵字。ElementType指定這個
channel所能傳遞的元素類型。舉個例子,咱們聲明一個傳遞類型爲int的channel:
var ch chan int
或者,咱們聲明一個map,元素是bool型的channel:
var m map[string] chan bool
上面的語句都是合法的。
定義一個channel也很簡單,直接使用內置的函數make()便可:
ch := make(chan int)
這就聲明並初始化了一個int型的名爲ch的channel。
在channel的用法中,最多見的包括寫入和讀出。將一個數據寫入(發送)至channel的語法
很直觀,以下:
ch <- value
向channel寫入數據一般會致使程序阻塞,直到有其餘goroutine從這個channel中讀取數據。從
channel中讀取數據的語法是
value := <-ch
若是channel以前沒有寫入數據,那麼從channel中讀取數據也會致使程序阻塞,直到channel
中被寫入數據爲止。咱們以後還會提到如何控制channel只接受寫或者只容許讀取,即單向
channel。
4.5.2 select
早在Unix時代,select機制就已經被引入。經過調用select()函數來監控一系列的文件句
柄,一旦其中一個文件句柄發生了IO動做,該select()調用就會被返回。後來該機制也被用於
實現高併發的Socket服務器程序。Go語言直接在語言級別支持select關鍵字,用於處理異步IO
問題。
select的用法與switch語言很是相似,由select開始一個新的選擇塊,每一個選擇條件由
case語句來描述。與switch語句能夠選擇任何可以使用相等比較的條件相比,select有比較多的
限制,其中最大的一條限制就是每一個case語句裏必須是一個IO操做,大體的結構以下:
select {
case <-chan1:
// 若是chan1成功讀到數據,則進行該case處理語句
case chan2 <- 1:
// 若是成功向chan2寫入數據,則進行該case處理語句
default:
// 若是上面都沒有成功,則進入default處理流程
}
能夠看出,select不像switch,後面並不帶判斷條件,而是直接去查看case語句。每一個
case語句都必須是一個面向channel的操做。好比上面的例子中,第一個case試圖從chan1讀取
一個數據並直接忽略讀到的數據,而第二個case則是試圖向chan2中寫入一個整型數1,若是這
二者都沒有成功,則到達default語句。
基於此功能,咱們能夠實現一個有趣的程序:
ch := make(chan int, 1)
for {
select {
case ch <- 0:
case ch <- 1:
}
i := <-ch
fmt.Println("Value received:", i)
}
能看明白這段代碼的含義嗎?其實很簡單,這個程序實現了一個隨機向ch中寫入一個0或者1
的過程。固然,這是個死循環。
4.5.3 緩衝機制
以前咱們示範建立的都是不帶緩衝的channel,這種作法對於傳遞單個數據的場景能夠接受,
但對於須要持續傳輸大量數據的場景就有些不合適了。接下來咱們介紹如何給channel帶上緩衝,
從而達到消息隊列的效果。
要建立一個帶緩衝的channel,其實也很是容易:
c := make(chan int, 1024)
在調用make()時將緩衝區大小做爲第二個參數傳入便可,好比上面這個例子就建立了一個大小
爲1024的int類型channel,即便沒有讀取方,寫入方也能夠一直往channel裏寫入,在緩衝區被
填完以前都不會阻塞。
4.5.4 超時機制
在以前對channel的介紹中,咱們徹底沒有提到錯誤處理的問題,而這個問題顯然是不能被忽
略的。在併發編程的通訊過程當中,最須要處理的就是超時問題,即向channel寫數據時發現channel
已滿,或者從channel試圖讀取數據時發現channel爲空。若是不正確處理這些狀況,極可能會導
致整個goroutine鎖死。
雖然goroutine是Go語言引入的新概念,但通訊鎖死問題已經存在很長時間,在以前的C/C++
開發中也存在。操做系統在提供此類系統級通訊函數時也會考慮入超時場景,所以這些方法一般
都會帶一個獨立的超時參數。超過設定的時間時,仍然沒有處理完任務,則該方法會當即終止並
返回對應的超時信息。超時機制自己雖然也會帶來一些問題,好比在運行比較快的機器或者高速
的網絡上運行正常的程序,到了慢速的機器或者網絡上運行就會出問題,從而出現結果不一致的
現象,但從根本上來講,解決死鎖問題的價值要遠大於所帶來的問題。
使用channel時須要當心,好比對於如下這個用法:
i := <-ch
不出問題的話一切都正常運行。但若是出現了一個錯誤狀況,即永遠都沒有人往ch裏寫數據,那
麼上述這個讀取動做也將永遠沒法從ch中讀取到數據,致使的結果就是整個goroutine永遠阻塞並
沒有挽回的機會。若是channel只是被同一個開發者使用,那樣出問題的可能性還低一些。但若是
一旦對外公開,就必須考慮到最差的狀況並對程序進行保護。
Go語言沒有提供直接的超時處理機制,但咱們能夠利用select機制。雖然select機制不是
專爲超時而設計的,卻能很方便地解決超時問題。由於select的特色是隻要其中一個case已經
完成,程序就會繼續往下執行,而不會考慮其餘case的狀況。
基於此特性,咱們來爲channel實現超時機制:
這樣使用select機制能夠避免永久等待的問題,由於程序會在timeout中獲取到一個數據
後繼續執行,不管對ch的讀取是否還處於等待狀態,從而達成1秒超時的效果。
這種寫法看起來是一個小技巧,但倒是在Go語言開發中避免channel通訊超時的最有效方法。
在實際的開發過程當中,這種寫法也須要被合理利用起來,從而有效地提升代碼質量。
4.5.5 channel的傳遞
須要注意的是,在Go語言中channel自己也是一個原生類型,與map之類的類型地位同樣,因
此channel自己在定義後也能夠經過channel來傳遞。
咱們可使用這個特性來實現*nix上很是常見的管道(pipe)特性。管道也是使用很是普遍
的一種設計模式,好比在處理數據時,咱們能夠採用管道設計,這樣能夠比較容易以插件的方式
增長數據的處理流程。
下面咱們利用channel可被傳遞的特性來實現咱們的管道。爲了簡化表達,咱們假設在管道中
傳遞的數據只是一個整型數,在實際的應用場景中這一般會是一個數據塊。
首先限定基本的數據結構:
首先限定基本的數據結構:
type PipeData struct {
value int
handler func(int) int
next chan int
}
而後咱們寫一個常規的處理函數。咱們只要定義一系列PipeData的數據結構並一塊兒傳遞給
這個函數,就能夠達到流式處理數據的目的:
func handle(queue chan *PipeData) {
for data := range queue {
data.next <- data.handler(data.value)
}
}
這裏咱們只給出了大概的樣子,限於篇幅再也不展開。同理,利用channel的這個可傳遞特性,
咱們能夠實現很是強大、靈活的系統架構。相比之下,在C++、Java、C#中,要達成這樣的效果,
一般就意味着要設計一系列接口。
與Go語言接口的非侵入式相似,channel的這些特性也能夠大大下降開發者的心智成本,用
一些比較簡單卻實用的方式來達成在其餘語言中須要使用衆多技巧才能達成的效果。













編程

相關文章
相關標籤/搜索