【Go語言】【16】GO語言的併發

       在寫該文以前一直猶豫,是把Go的併發寫的面面俱到顯得高大尚一些,仍是簡潔易懂一些?今天看到一個新員工在學習Java,忽然間想起第一次接觸Java的併發時,被做者搞了一個雲裏霧裏,直到如今還有陰影,因此決定本文從簡。哈哈,說笑了,言歸正傳。併發

       Go的併發真的很簡單,因此本文不羅嗦進程、線程、協程、信號量、鎖、調度、時間片等亂七八糟的東西,由於這些不影響您理解Go的併發。先看一個小例子:異步

package main


import "fmt"ide


func Add(i, j int) {學習

        sum := i + jui

        fmt.Println(i, " + ", j, " = ", sum)spa

}線程


func main() {翻譯

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

               Add(i, i)orm

        }

}

這個例子很簡單吧,說白了就是計算0+0、1+一、2+二、3+三、.......、9+9之和,並打印出來,運行結果顯而易見:

wKiom1WxDbuytM6ZAAB3g9O_6RE137.jpg


這裏沒有使用併發呀,好吧,爲了提高計算效率,在main的for循環中使用併發,把代碼修改以下:

package main


import "fmt"


func Add(i, j int) {

        sum := i + j

        fmt.Println(i, " + ", j, " = ", sum)

}


func main() {

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

               go Add(i, i)

        }

}


沒有花眼吧,前面加了一個go,這就併發了?

嗯,這就併發了。

在方法Add()以前增長了一個關鍵字go,至關於告訴Go編譯器啓動一個goroutine,而後把Add()方法放到goroutine中執行。


什麼是goroutine?

有人把它翻譯爲協程,說實話挺反感的,有些單詞仍是不要翻譯爲好,好比Context,常常寫Web程序的人會遇到,有人把它翻譯爲上下文;再如payload,常常作***的人會使用,怎麼翻譯好呢?仍是不翻譯了吧。


該怎麼理解goroutine?

wKiom1WxFNSzzoWGAABCVx7S4ag290.jpg

如上圖所示,main()方法所在的goroutine上又建立了10個goroutine,每一個goroutine各自跑一個Add()方法


OK,運行一下該程序,結果以下:

wKioL1WyPvPxvZKQAAA6SKx6fxY454.jpg

咦,怎麼都沒有,說好的運行結果呢?

       這是由於main()所在的goroutine建立10個goroutine後,它裏面的邏輯已執行完,那麼main()就退出了,它根本就無論這10個goroutine的死活。從結果也能看出,當main()退出時,這10個goroutine沒有一個執行完,因此結果什麼都沒有打印。


如何解決這個問題呢?

一種比較容易想到的但又比較垃圾的解決辦法是:「讓main()等一下子」,下面咱們修改一個這個程序

package main


import (

       "fmt"

       "time"

)


func Add(i, j int) {

        sum := i + j

        fmt.Println(i, " + ", j, " = ", sum)

}


func main() {

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

               go Add(i, i)

        }


        time.Sleep(time.Second * 3)  // main()所在goroutine休息3秒鐘

}

首先引入"time"這個包,而後調用time.Sleep()方法,讓main()所在goroutine等3s,等其它10個goroutine都運行完,這種解決辦法就是「馬兒你慢些跑呀慢些跑」 :)

運行一下結果:

wKioL1W0il2j7fDCAACEihWG434218.jpg


可能您會問,爲什麼要等3秒鐘而不是2秒鐘?

我只能學着印度老外,一邊搖頭一邊微笑地告訴您,我是蒙的,由於我也不知道確切地等多長時間,因此是一種垃圾的解決辦法。


那有沒有一種通知機制呢?當一個goroutine執行完畢後,就告訴主goroutine(即main()方法所在的goroutine):「嘿,哥們,我執行完了,你想幹嗎就幹嗎吧!」

有,這就是Go語言的亮點,十分耀眼的一個亮點:channel


什麼是channel?

說白了就是一個通道(建議仍是不翻譯的爲好),一個goroutine執行完畢後,就告訴主goroutine,I'm over!怎麼告訴呢?就是向channel中寫一個數據。


怎麼向channel中寫一個數據呢?

OK,follow me,要想寫一個數據到channel則必須有一個channel不是?因此:

(1)創建一個channel


【備註】:所謂channel也是一種Go的類型,與int、float6四、string、bool、struct、slice、map等同等地位


var ch chan int           // 聲明一個變量爲ch,它的類型爲chan類型,這個channel裏面能夠存放int型的值

ch = make(chan int)  // 使用make關鍵字初始一個長度爲0的通道

固然聲明和初始化能夠一塊來

var ch chan int = make(chan int)


(2)向channel中寫一個數據

ch <- 1

就這麼簡單,使用符號」<-「,前面聲明瞭channel的類型爲int ,因此就把1寫入ch;若聲明channel類型爲bool,就能夠把布爾值寫入channel,即ch <- true


(3)從channel中讀數據

<- ch

嗯,仍是這麼簡單

不管寫仍是讀都是用符號」<-「,就看<-後面是誰


OK,既然知道有channel這東東了,咱們修改一下上面的程序:

package main


import (

        "fmt"

        // "time"   // 刪除掉time包

)


var ch chan int = make(chan int)     // 初始化一個類型爲channel的變量ch,其中channel裏面放int型數據


func Add(i, j int) {

        sum := i + j

        fmt.Println(i, " + ", j, " = ", sum)

        ch <- i   // 這個方法執行完,意味着方法所屬的goroutine即將退出,就告訴主goroutine,I'm完事了

}


func main() {

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

               go Add(i, i)

        }


        /*

         * 因爲有10個goroutine,因此從channel中讀10次

         * 這10個goroutine都告訴主gorouinte說完事了,那麼主gorouinte也就退出了

         */

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

               fmt.Println("i=", <-ch)

        }

  

        // time.Sleep(time.Second * 3)    // 不用這種機制了,留着也沒有用

}

運行結果以下:

wKiom1W0j0Pywr5YAACuV9q2SiA269.jpg


目的達到了,我好人作到底,再解釋一下:

wKiom1W0kT6iWzBaAABBf0JPAyg153.jpg

能夠這樣理解,有10個廚師1個端菜工,這10個廚師各自作各自的菜,作完以後就放到channel,這個端菜工就從channel中取菜。當channel中沒有菜時,端菜工就一直等待直到有菜爲止;廚師作好一個菜後,發現channel中沒有菜,就把本身的菜放到channel中,若發現channel在有菜尚未端正,廚師就拿着本身的菜一直等到channel中的菜被端菜工端走後再把本身的菜放進去。


可能您又要說了,這種channel很相似同步操做,這個channel只能放一個菜,端菜工端一個菜;channel中沒有菜端菜工等待;channel中有菜廚師等待。效率不高呀。

好吧,Go設計師已提早爲您想好處理辦法了,即這個channel能夠放10個菜,只要這個channel尚未放滿10個菜,廚師就能夠向上面放,這樣廚師就不用等待了。剩下的就是端菜工要提升本身的工做效率了。

怎麼放10個菜?

var ch chan int = make(chan int,10)

OK,搞定!代碼以下:

package main


import (

       "fmt"

       // "time"

)


var ch chan int = make(chan int, 10)


func Add(i, j int) {

        sum := i + j

        fmt.Println(i, " + ", j, " = ", sum)

        ch <- i

}


func main() {

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

                go Add(i, i)

        }


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

                fmt.Println("i=", <-ch)

        }


        // time.Sleep(time.Second * 3)

}

運行結果以下:

wKioL1W0lmyg4e6pAAC0isrnOiA330.jpg

仔細看,再仔細看,看出什麼東西來了沒有?若沒有,請與上一個運行結果對比着看 :)

仍是沒有看來?

沒有發現這兩個基本上是如出一轍的嗎?除了運行程序所花的時間不一樣以外!


從運行時間上來看,好像效率提高了一些,這是由於廚師不用等待了,一旦指明瞭channel的容量,相對於廚師來講就變成異步的了;沒有指明channel容量,相對於廚師來講就是同步的。


同步異步不是我想讓您觀察的重點,您難道沒有發現0+0、1+一、2+二、3+三、......、9+9,這個順序太正常了嗎?若真正併發的話,這個順序確定是亂的!

看一下我電腦信息:

wKioL1W0mP_zj3AJAACAK6QNaAg573.jpg

好呆CPU也是四核的,併發的順序是這麼的正常,太難以想象了 :)


好吧,我再解開這謎團吧

我用的Go版本是1.4,能夠在命令窗口中執行go version查看。因爲Go語言1.4版本對多核的處理尚未作太多的改進,聽說1.5版本有突破,後面能夠關注一下,因此在這個版本仍是使用的一個核。對於單核CPU進程、線程在執行的過程當中,系統會把運行的CPU強行切給另外一個進程或線程。

       有人若是看過其餘人的博客、書,都會把goroutine翻譯爲協程,所謂協程就是用戶態的線程,能夠這樣理解:」一個協程在執行時,系統不會強行切換時間片「。即一個goroutine在執行的過程當中,Go語言會讓這個goroutine瘋狂地執行,直到它運行完爲止,再讓另一個gorouinte運行,因此從結果來看運行順序是固定的。


若是利用多核?

Go語言也提供了一種方式,具體代碼以下:

package main


import (

        "fmt"

         // "time"

        "runtime"   // 引入runtime包

)


var ch chan int = make(chan int, 10)


func Add(i, j int) {

        sum := i + j

        fmt.Println(i, " + ", j, " = ", sum)

        ch <- i

}


func main() {

        runtime.GOMAXPROCS(runtime.NumCPU())   // 讓Go使用多個核


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

                go Add(i, i)

        }


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

                fmt.Println("i=", <-ch)

        }


        // time.Sleep(time.Second * 3)

}

多運行幾回,結果以下:

wKioL1W0nP7Dn8CjAADDrRYGui4629.jpg


上面就是Go的併發核心內容,固然還有關於併發的其它內容,如單向寫channel、單向讀channel、傳統併發方式等內容。本文就先寫到這裏,內容多了不容易消化 :)

相關文章
相關標籤/搜索