Go語言並發之美:解釋其中核心、外延

來源:互聯網
上載者:User

多核處理器越來越普及,那有沒有一種簡單的辦法,能夠讓我們寫的軟體釋放多核的威力。答案是:Yes。隨著Golang, Erlang, Scale等為並發設計的程式語言的興起,新的併發模式逐漸清晰。正如過程式編程和物件導向一樣,一個好的編程模式需要有一個極其簡潔的核心,還有在此之 上豐富的外延,可以解決現實世界中各種各樣的問題。本文以GO語言為例,解釋其中核心、外延。

併發模式之核心

這種併發模式的核心只需要協程和通道就夠了。其中協程負責執行代碼,通道負責在協程之間傳遞事件。

  

並發編程一直以來都是個非常困難的工作。要想編寫一個良好的並發程式,我們不得不瞭解線程, 鎖,semaphore,barrier甚至CPU更新快取的方式,而且他們個個都有怪脾氣,處處是陷阱。筆者除非萬不得以,決不會自己操作這些底層 並發元素。一個簡潔的併發模式不需要這些複雜的底層元素,只需協程和通道就夠了。

協程是輕量級的線程。在過程式編程中,當調用一個過程的時候,需要等待其執行完才返回。而調用一個協程的時候,不需要等待其執行完,會立即返回。協程十分 輕量,Go語言可以在一個進程中執行有數以十萬計的協程,依舊保持高效能。而對於普通的平台,一個進程有數千個線程,其CPU會忙於環境切換,效能急劇 下降。隨意建立線程可不是一個好主意,但是我們可以大量使用的協程。

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

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

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

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

  

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

併發模式之外延

協程相較於線程,可以大量建立。開啟這扇門,我們拓展出新的用法,可以做產生器,可以讓函數返回“服務”,可以讓迴圈並發執行,還能共用變數。但是出現新 的用法的同時,也帶來了新的棘手問題,協程也會泄漏,不恰當的使用會影響效能。下面會逐一介紹各種用法和問題。示範的代碼用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'.

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.