站長資訊網
        最全最豐富的資訊網站

        go語言中并發圖文教程

        go語言中并發圖文教程

        正如過程式編程和面向對象一樣,一個好的編程模式需要有一個極其簡潔的內核,還有在此之上豐富的外延,可以解決現實世界中各種各樣的問題。本文以GO語言為例,解釋其中內核、外延。

        并發模式之內核

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

        go語言中并發圖文教程

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

        協程是輕量級的線程。在過程式編程中,當調用一個過程的時候,需要等待其執行完才返回。而調用一個協程的時候,不需要等待其執行完,會立即返回。

        協程十分輕量,Go語言可以在一個進程中執行有數以十萬計的協程,依舊保持高性能。而對于普通的平臺,一個進程有數千個線程,其CPU會忙于上下文切換,性能急劇下降。隨意創建線程可不是一個好主意,但是我們可以大量使用的協程。

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

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

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

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

        go語言中并發圖文教程

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

        并發模式之外延

        協程相較于線程,可以大量創建。打開這扇門,我們拓展出新的用法,可以做生成器,可以讓函數返回“服務”,可以讓循環并發執行,還能共享變量。但是出現新 的用法的同時,也帶來了新的棘手問題,協程也會泄漏,不恰當的使用會影響性能。下面會逐一介紹各種用法和問題。演示的代碼用GO語言寫成,因為其簡潔明 了,而且支持全部功能。

        1、生成器

        有的時候,我們需要有一個函數能不斷生成數據。比方說這個函數可以讀文件,讀網絡,生成自增長序列,生成隨機數。這些行為的特點就是,函數的已知一些變量,如文件路徑。然后不斷調用,返回新的數據。

        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("%dn",<-rand_service_handler) }

        上面的這段函數就可以并發執行了rand.Int()。有一點值得注意到函數的返回可以理解為一個“服務”。但我們需要獲取隨機數據時候,可以隨時向這個 服務取用,他已經為我們準備好了相應的數據,無需等待,隨要隨到。

        如果我們調用這個服務不是很頻繁,一個協程足夠滿足我們的需求了。但如果我們需要大量訪問,怎么辦?我們可以用下面介紹的多路復用技術,啟動若干生成器,再將其整合成一個大的服務。

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

        2、多路復用

        多路復用是讓一次處理多個隊列的技術。Apache使用處理每個連接都需要一個進程,所以其并發性能不是很好。而Nginx使用多路復用的技術,讓一 個進程處理多個連接,所以并發性能比較好。

        同樣,在協程的場合,多路復用也是需要的,但又有所不同。多路復用可以將若干個相似的小服務整合成一個大服務。

        go語言中并發圖文教程

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

        // 函數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關鍵字來解決,各家也有各家竅門。加大輸出通道的緩沖大小是個通用的解決方法。

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

        3、Future技術

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

        go語言中并發圖文教程

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

        //一個查詢結構體 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是一個非常強大的技術手段。可以在調用的時候不關心數據是否準備好,返回值是否計算好的問題。讓程序中的組件在準備好數據的時候自動跑起來。

        4、并發循環

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

        go語言中并發圖文教程

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

        //建立計數器 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熱點。正因為協程可以大量創建,才能在循環體中如此使用,如果是使用線程的話,就需要引入線程池之類的東西,防止創建過多線程,而協程則簡單的多。

        5、ChainFilter技術

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

        go語言中并發圖文教程

        由于每個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通過簡單的代碼創建并發的過濾器鏈。這種辦法還有一個好處,就是每個通道只有兩個協程會訪問,就不會有激烈的競爭,性能會比較好

        6、共享變量

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

        go語言中并發圖文教程

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

        //共享變量有一個讀通道和一個寫通道組成 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) }

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

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

        7、協程泄漏

        協程和內存一樣,是系統的資源。對于內存,有自動垃圾回收。但是對于協程,沒有相應的回收機制。會不會若干年后,協程普及了,協程泄漏和內存泄漏一樣成為 程序員永遠的痛呢?

        一般而言,協程執行結束后就會銷毀。協程也會占用內存,如果發生協程泄漏,影響和內存泄漏一樣嚴重。輕則拖慢程序,重則壓垮機器。

        C和C++都是沒有自動內存回收的程序設計語言,但只要有良好的編程習慣,就能解決規避問題。對于協程是一樣的,只要有好習慣就可以了。

        只有兩種情況會導致協程無法結束。一種情況是協程想從一個通道讀數據,但無人往這個通道寫入數據,或許這個通道已經被遺忘了。還有一種情況是程想往一個通道寫數據,可是由于無人監聽這個通道,該協程將永遠無法向下執行。下面分別討論如何避免這兩種情況。

        對于協程想從一個通道讀數據,但無人往這個通道寫入數據這種情況。解決的辦法很簡單,加入超時機制。對于有不確定會不會返回的情況,必須加入超時,避免出 現永久等待。

        另外不一定要使用定時器才能終止協程。也可以對外暴露一個退出提醒通道。任何其他協程都可以通過該通道來提醒這個協程終止。

        go語言中并發圖文教程

        對于協程想往一個通道寫數據,但通道阻塞無法寫入這種情況。解決的辦法也很簡單,就是給通道加緩沖。但前提是這個通道只會接收到固定數目的寫入。

        比方說, 已知一個通道最多只會接收N次數據,那么就將這個通道的緩沖設置為N。那么該通道將永遠不會堵塞,協程自然也不會泄漏。也可以將其緩沖設置為無限,不過這 樣就要承擔內存泄漏的風險了。等協程執行完畢后,這部分通道內存將會失去引用,會被自動垃圾回收掉。

        funcnever_leak(ch chan int) {          //初始化timeout,緩沖為1          timeout := make(chan bool, 1)          //啟動timeout協程,由于緩存為1,不可能泄露          go func() {                    time.Sleep(1 * time.Second)                    timeout <- true          }()          //監聽通道,由于設有超時,不可能泄露          select {          case <-ch:                    // a read from ch hasoccurred          case <-timeout:                    // the read from ch has timedout          } }

        上面是個避免泄漏例子。使用超時避免讀堵塞,使用緩沖避免寫堵塞。

        和內存里面的對象一樣,對于長期存在的協程,我們不用擔心泄漏問題。一是長期存在,二是數量較少。要警惕的只有那些被臨時創建的協程,這些協程數量大且生 命周期短,往往是在循環中創建的,要應用前面提到的辦法,避免泄漏發生。協程也是把雙刃劍,如果出問題,不但沒能提高程序性能,反而會讓程序崩潰。但就像 內存一樣,同樣有泄漏的風險,但越用越溜了。

        并發模式之實現

        在并發編程大行其道的今天,對協程和通道的支持成為各個平臺比不可少的一部分。雖然各家有各家的叫法,但都能滿足協程的基本要求—并發執行和可大量創建。筆者對他們的實現方式總結了一下。

        下面列舉一些已經支持協程的常見的語言和平臺。

        go語言中并發圖文教程

        GoLang 和Scala作為最新的語言,一出生就有完善的基于協程并發功能。Erlang最為老資格的并發編程語言,返老還童。其他二線語言則幾乎全部在新的版本中加入了協程。

        令人驚奇的是C/C++和Java這三個世界上最主流的平臺沒有在對協程提供語言級別的原生支持。他們都背負著厚重的歷史,無法改變,也無需改變。但他們還有其他的辦法使用協程。

        Java平臺有很多方法實現協程:

        · 修改虛擬機:對JVM打補丁來實現協程,這樣的實現效果好,但是失去了跨平臺的好處

        · 修改字節碼:在編譯完成后增強字節碼,或者使用新的JVM語言。稍稍增加了編譯的難度。

        · 使用JNI:在Jar包中使用JNI,這樣易于使用,但是不能跨平臺。

        · 使用線程模擬協程:使協程重量級,完全依賴JVM的線程實現。

        其中修改字節碼的方式比較常見。因為這樣的實現辦法,可以平衡性能和移植性。最具代表性的JVM語言Scale就能很好的支持協程并發。流行的Java Actor模型類庫akka也是用修改字節碼的方式實現的協程。

        對于C語言,協程和線程一樣。可以使用各種各樣的系統調用來實現。協程作為一個比較高級的概念,實現方式實在太多,就不討論了。比較主流的實現有libpcl, coro,lthread等等。

        對于C++,有Boost實現,還有一些其他開源庫。還有一門名為μC++語言,在C++基礎上提供了并發擴展。

        可見這種編程模型在眾多的語言平臺中已經得到了廣泛的支持,不再小眾。如果想使用的話,隨時可以加到自己的工具箱中。

        贊(0)
        分享到: 更多 (0)
        網站地圖   滬ICP備18035694號-2    滬公網安備31011702889846號
        主站蜘蛛池模板: 亚洲精品A在线观看| 国内精品伊人久久久久av一坑| 少妇人妻偷人精品免费视频| 亚洲一区精品中文字幕| 国产精品毛片VA一区二区三区| 亚洲国产精品一区二区九九| 国产精品小黄鸭一区二区三区| 国产精品久久久久9999高清| 欧洲精品久久久av无码电影| 日本精品久久久久影院日本| 国产精品久久久99| 亚洲一二成人精品区| 精品一区二区三区中文字幕| 国产精品人成在线播放新网站 | 精品少妇无码AV无码专区| 88国产精品无码一区二区三区| 午夜影视日本亚洲欧洲精品一区| 国产欧美日韩精品丝袜高跟鞋 | 国产精品午夜久久| 91在线手机精品超级观看| 久久精品国产一区| 精品久久一区二区三区| 2022国产精品自产拍在线观看| 精品国产一区二区三区不卡| 久久精品国产亚洲av影院| 久久久久人妻精品一区二区三区| 最新国产在线精品观看| 亚洲精品无码你懂的网站| 日本欧美国产精品第一页久久| 精品亚洲欧美无人区乱码| 国产精品午夜久久| 精品视频一区二区三区| 久久精品国产亚洲Aⅴ香蕉| 久久e热在这里只有国产中文精品99 | 久久精品国产亚洲av瑜伽| 精品成人av一区二区三区| 国产乱人伦偷精品视频| 久久久久久久久久久免费精品| 欧美精品播放| 亚洲AV永久精品爱情岛论坛| 精品久久久久久无码专区不卡|