歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux編程 >> Linux編程 >> Go語言並發之美

Go語言並發之美

日期:2017/3/1 9:35:55   编辑:Linux編程
簡介 多核處理器越來越普及,那有沒有一種簡單的辦法,能夠讓我們寫的軟件釋放多核的威力?答案是:Yes。隨著Golang, Erlang, Scale等為並發設計的程序語言的興起,新的並發模式逐漸清晰。正如過程式編程和面向對象一樣,一個好的編程模式需要有一個極其簡潔的內核,還有在此之上豐富的外延,可以解決現實世界中各種各樣的問題。本文以GO語言為例,解釋其中內核、外延。 並發模式之內核 這種並發模式的內核只需要協程和通道就夠了。其中協程負責執行代碼,通道負責在協程之間傳遞事件。 查看大圖 並發編程一直以來都是個非常困難的工作。要想編寫一個良好的並發程序,我們不得不了解線程,鎖,semaphore,barrier甚至CPU更新高速緩存的方式,而且他們個個都有怪脾氣,處處是陷阱。筆者除非萬不得以,決不會自己操作這些底層並發元素。一個簡潔的並發模式不需要這些復雜的底層元素,只需協程和通道就夠了。 協程是輕量級的線程。在過程式編程中,當調用一個過程的時候,需要等待其執行完才返回。而調用一個協程的時候,不需要等待其執行完,會立即返回。協程十分輕量,Go語言可以在一個進程中執行有數以十萬計的協程,依舊保持高性能。而對於普通的平台,一個進程有數千個線程,其CPU會忙於上下文切換,性能急劇下降。隨意創建線程可不是一個好主意,但是我們可以大量使用的協程。

通道是協程之間的數據傳輸通道。通道可以在眾多的協程之間傳遞數據,具體可以值也可以是個引用。通道有兩種使用方式。

· 協程可以試圖向通道放入數據,如果通道滿了,會掛起協程,直到通道可以為他放入數據為止。

· 協程可以試圖向通道索取數據,如果通道沒有數據,會掛起協程,直到通道返回數據為止。

如此,通道就可以在傳遞數據的同時,控制協程的運行。有點像事件驅動,也有點像阻塞隊列。這兩個概念非常的簡單,各個語言平台都會有相應的實現。在Java和C上也各有庫可以實現兩者。

查看大圖 只要有協程和通道,就可以優雅的解決並發的問題。不必使用其他和並發有關的概念。那如何用這兩把利刃解決各式各樣的實際問題呢?

Ubuntu 安裝Go語言包 http://www.linuxidc.com/Linux/2013-05/85171.htm

《Go語言編程》高清完整版電子書 http://www.linuxidc.com/Linux/2013-05/84709.htm

Go語言並行之美 -- 超越 “Hello World” http://www.linuxidc.com/Linux/2013-05/83697.htm

我為什麼喜歡Go語言 http://www.linuxidc.com/Linux/2013-05/84060.htm

Go語言內存分配器的實現 http://www.linuxidc.com/Linux/2014-01/94766.htm

Go語言的國際化支持(基於gettext-go) http://www.linuxidc.com/Linux/2014-01/94917.htm

並發模式之外延 協程相較於線程,可以大量創建。打開這扇門,我們拓展出新的用法,可以做生成器,可以讓函數返回“服務”,可以讓循環並發執行,還能共享變量。但是出現新的用法的同時,也帶來了新的棘手問題,協程也會洩漏,不恰當的使用會影響性能。下面會逐一介紹各種用法和問題。演示的代碼用GO語言寫成,因為其簡潔明了,而且支持全部功能。 生成器 有的時候,我們需要有一個函數能不斷生成數據。比方說這個函數可以讀文件,讀網絡,生成自增長序列,生成隨機數。這些行為的特點就是,函數的已知一些變量,如文件路徑。然後不斷調用,返回新的數據。 查看大圖

下面生成隨機數為例,以讓我們做一個會並發執行的隨機數生成器。

非並發的做法是這樣的:

// 函數rand_generator_1 ,返回 int

funcrand_generator_1() int {

return rand.Int()

} 上面是一個函數,返回一個int。假如rand.Int()這個函數調用需要很長時間等待,那該函數的調用者也會因此而掛起。所以我們可以創建一個協程,專門執行rand.Int()。

// 函數rand_generator_2,返回通道(Channel)

funcrand_generator_2() chan int {

// 創建通道

out := make(chan int)

// 創建協程

go func() {

for {

//向通道內寫入數據,如果無人讀取會等待

out <- rand.Int()

}

}()

return out

}

funcmain() {

// 生成隨機數作為一個服務

rand_service_handler :=rand_generator_2()

// 從服務中讀取隨機數並打印

fmt.Printf("%d\n",<-rand_service_handler)

}

上面的這段函數就可以並發執行了rand.Int()。有一點值得注意到函數的返回可以理解為一個“服務”。但我們需要獲取隨機數據時候,可以隨時向這個服務取用,他已經為我們准備好了相應的數據,無需等待,隨要隨到。如果我們調用這個服務不是很頻繁,一個協程足夠滿足我們的需求了。但如果我們需要大量訪問,怎麼辦?我們可以用下面介紹的多路復用技術,啟動若干生成器,再將其整合成一個大的服務。

調用生成器,可以返回一個“服務”。可以用在持續獲取數據的場合。用途很廣泛,讀取數據,生成ID,甚至定時器。這是一種非常簡潔的思路,將程序並發化。

多路復用

多路復用是讓一次處理多個隊列的技術。Apache使用處理每個連接都需要一個進程,所以其並發性能不是很好。而Nginx使用多路復用的技術,讓一個進程處理多個連接,所以並發性能比較好。同樣,在協程的場合,多路復用也是需要的,但又有所不同。多路復用可以將若干個相似的小服務整合成一個大服務。

查看大圖

那麼讓我們用多路復用技術做一個更高並發的隨機數生成器吧。

// 函數rand_generator_3 ,返回通道(Channel)

funcrand_generator_3() chan int {

// 創建兩個隨機數生成器服務

rand_generator_1 := rand_generator_2()

rand_generator_2 := rand_generator_2()

//創建通道

out := make(chan int)

//創建協程

go func() {

for {

//讀取生成器1中的數據,整合

out <-<-rand_generator_1

}

}()

go func() {

for {

//讀取生成器2中的數據,整合

out <-<-rand_generator_2

}

}()

return out

}

上面是使用了多路復用技術的高並發版的隨機數生成器。通過整合兩個隨機數生成器,這個版本的能力是剛才的兩倍。雖然協程可以大量創建,但是眾多協程還是會爭搶輸出的通道。Go語言提供了Select關鍵字來解決,各家也有各家竅門。加大輸出通道的緩沖大小是個通用的解決方法。

多路復用技術可以用來整合多個通道。提升性能和操作的便捷。配合其他的模式使用有很大的威力。

Future技術

Future是一個很有用的技術,���們常常使用Future來操作線程。我們可以在使用線程的時候,可以創建一個線程,返回Future,之後可以通過它等待結果。 但是在協程環境下的Future可以更加徹底,輸入參數同樣可以是Future的。

查看大圖

調用一個函數的時候,往往是參數已經准備好了。調用協程的時候也同樣如此。但是如果我們將傳入的參數設為通道,這樣我們就可以在不准備好參數的情況下調用函數。這樣的設計可以提供很大的自由度和並發度。函數調用和函數參數准備這兩個過程可以完全解耦。下面舉一個用該技術訪問數據庫的例子。

//一個查詢結構體

typequery struct {

//參數Channel

sql chan string

//結果Channel

result chan string

}

//執行Query

funcexecQuery(q query) {

//啟動協程

go func() {

//獲取輸入

sql := <-q.sql

//訪問數據庫,輸出結果通道

q.result <- "get" + sql

}()

}

funcmain() {

//初始化Query

q :=

query{make(chan string, 1),make(chan string, 1)}

//執行Query,注意執行的時候無需准備參數

execQuery(q)

//准備參數

q.sql <- "select * fromtable"

//獲取結果

fmt.Println(<-q.result)

}

上面利用Future技術,不單讓結果在Future獲得,參數也是在Future獲取。准備好參數後,自動執行。Future和生成器的區別在於,Future返回一個結果,而生成器可以重復調用。還有一個值得注意的地方,就是將參數Channel和結果Channel定義在一個結構體裡面作為參數,而不是返回結果Channel。這樣做可以增加聚合度,好處就是可以和多路復用技術結合起來使用。

Future技術可以和各個其他技術組合起來用。可以通過多路復用技術,監聽多個結果Channel,當有結果後,自動返回。也可以和生成器組合使用,生成器不斷生產數據,Future技術逐個處理數據。Future技術自身還可以首尾相連,形成一個並發的pipe filter。這個pipe filter可以用於讀寫數據流,操作數據流。

Future是一個非常強大的技術手段。可以在調用的時候不關心數據是否准備好,返回值是否計算好的問題。讓程序中的組件在准備好數據的時候自動跑起來。

並發循環

循環往往是性能上的熱點。如果性能瓶頸出現在CPU上的話,那麼九成可能性熱點是在一個循環體內部。所以如果能讓循環體並發執行,那麼性能就會提高很多。

查看大圖

要並發循環很簡單,只有在每個循環體內部啟動協程。協程作為循環體可以並發執行。調用啟動前設置一個計數器,每一個循環體執行完畢就在計數器上加一個元素,調用完成後通過監聽計數器等待循環協程全部完成。

//建立計數器

sem :=make(chan int, N);

//FOR循環體

for i,xi:= range data {

//建立協程

go func (i int, xi float) {

doSomething(i,xi);

//計數

sem <- 0;

} (i, xi);

}

// 等待循環結束

for i := 0; i < N; ++i { <-sem }

上面是一個並發循環例子。通過計數器來等待循環全部完成。如果結合上面提到的Future技術的話,則不必等待。可以等到真正需要的結果的地方,再去檢查數據是否完成。

通過並發循環可以提供性能,利用多核,解決CPU熱點。正因為協程可以大量創建,才能在循環體中如此使用,如果是使用線程的話,就需要引入線程池之類的東西,防止創建過多線程,而協程則簡單的多。

ChainFilter技術

前面提到了Future技術首尾相連,可以形成一個並發的pipe filter。這種方式可以做很多事情,如果每個Filter都由同一個函數組成,還可以有一種簡單的辦法把他們連起來。

查看大圖

由於每個Filter協程都可以並發運行,這樣的結構非常有利於多核環境。下面是一個例子,用這種模式來產生素數。

// Aconcurrent prime sieve

packagemain

// Sendthe sequence 2, 3, 4, ... to channel 'ch'.

funcGenerate(ch chan<- int) {

for i := 2; ; i++ {

ch<- i // Send 'i' to channel 'ch'.

}

}

// Copythe values from channel 'in' to channel 'out',

//removing those divisible by 'prime'.

funcFilter(in <-chan int, out chan<- int, prime int) {

for {

i := <-in // Receive valuefrom 'in'.

if i%prime != 0 {

out <- i // Send'i' to 'out'.

}

}

}

// Theprime sieve: Daisy-chain Filter processes.

funcmain() {

ch := make(chan int) // Create a newchannel.

go Generate(ch) // Launch Generate goroutine.

for i := 0; i < 10; i++ {

prime := <-ch

print(prime, "\n")

ch1 := make(chan int)

go Filter(ch, ch1, prime)

ch = ch1

}

}

上面的程序創建了10個Filter,每個分別過濾一個素數,所以可以輸出前10個素數。

Chain-Filter通過簡單的代碼創建並發的過濾器鏈。這種辦法還有一個好處,就是每個通道只有兩個協程會訪問,就不會有激烈的競爭,性能會比較好。

共享變量

協程之間的通信只能夠通過通道。但是我們習慣於共享變量,而且很多時候使用共享變量能讓代碼更簡潔。比如一個Server有兩個狀態開和關。其他僅僅希望獲取或改變其狀態,那又該如何做呢。可以將這個變量至於0通道中,並使用一個協程來維護。

查看大圖

下面的例子描述如何用這個方式,實現一個共享變量。

//共享變量有一個讀通道和一個寫通道組成

typesharded_var struct {

reader chan int

writer chan int

}

//共享變量維護協程

funcsharded_var_whachdog(v sharded_var) {

go func() {

//初始值

var value int = 0

for {

//監聽讀寫通道,完成服務

select {

case value =<-v.writer:

case v.reader <-value:

}

}

}()

}

funcmain() {

//初始化,並開始維護協程

v := sharded_var{make(chan int),make(chan int)}

sharded_var_whachdog(v)

//讀取初始值

fmt.Println(<-v.reader)

//寫入一個值

v.writer <- 1

//讀取新寫入的值

fmt.Println(<-v.reader)

}

這樣,就可以在協程和通道的基礎上實現一個協程安全的共享變量了。定義一個寫通道,需要更新變量的時候,往裡寫新的值。再定義一個讀通道,需要讀的時候,從裡面讀。通過一個單獨的協程來維護這兩個通道。保證數據的一致性。

一般來說,協程之間不推薦使用共享變量來交互,但是按照這個辦法,在一些場合,使用共享變量也是可取的。很多平台上有較為原生的共享變量支持,到底用那種實現比較好,就見仁見智了。另外利用協程和通道,可以還實現各種常見的並發數據結構,如鎖等等,就不一一贅述。

更多詳情見請繼續閱讀下一頁的精彩內容: http://www.linuxidc.com/Linux/2014-12/110965p2.htm

Copyright © Linux教程網 All Rights Reserved