天天看點

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

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

       Go的并發真的很簡單,是以本文不羅嗦程序、線程、協程、信号量、鎖、排程、時間片等亂七八糟的東西,因為這些不影響您了解Go的并發。先看一個小例子:

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++ {

               Add(i, i)

        }

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

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

這裡沒有使用并發呀,好吧,為了提升計算效率,在main的for循環中使用并發,把代碼修改如下:

               go Add(i, i)

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

嗯,這就并發了。

在方法Add()之前增加了一個關鍵字go,相當于告訴Go編譯器啟動一個goroutine,然後把Add()方法放到goroutine中執行。

什麼是goroutine?

有人把它翻譯為協程,說實話挺反感的,有些單詞還是不要翻譯為好,比如Context,經常寫Web程式的人會遇到,有人把它翻譯為上下文;再如payload,經常做滲透的人會使用,怎麼翻譯好呢?還是不翻譯了吧。

該怎麼了解goroutine?

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

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

OK,運作一下該程式,結果如下:

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

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

       這是因為main()所在的goroutine建立10個goroutine後,它裡面的邏輯已執行完,那麼main()就退出了,它根本就不管這10個goroutine的死活。從結果也能看出,當main()退出時,這10個goroutine沒有一個執行完,是以結果什麼都沒有列印。

如何解決這個問題呢?

一種比較容易想到的但又比較垃圾的解決辦法是:“讓main()等一會兒”,下面我們修改一個這個程式

import (

       "fmt"

       "time"

)

               go Add(i, i)

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

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

運作一下結果:

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

可能您會問,為何要等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、float64、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這東東了,我們修改一下上面的程式:

        "fmt"

        // "time"   // 删除掉time包

var ch chan int = make(chan int)     // 初始化一個類型為channel的變量ch,其中channel裡面放int型資料

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

        /*

         * 由于有10個goroutine,是以從channel中讀10次

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

         */

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

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

運作結果如下:

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

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

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

可以這樣了解,有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,搞定!代碼如下:

       // "time"

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

        ch <- i

                go Add(i, i)

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

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

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

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

還是沒有看來?

沒有發現這兩個基本上是一模一樣的嗎?除了運作程式所花的時間不同之外!

從運作時間上來看,好像效率提升了一些,這是因為廚師不用等待了,一旦指明了channel的容量,相對于廚師來說就變成異步的了;沒有指明channel容量,相對于廚師來說就是同步的。

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

看一下我電腦資訊:

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

好呆CPU也是四核的,并發的順序是這麼的正常,太不可思議了 :)

好吧,我再解開這謎團吧

我用的Go版本是1.4,可以在指令視窗中執行go version檢視。由于Go語言1.4版本對多核的處理還沒有做太多的改進,據說1.5版本有突破,後面可以關注一下,是以在這個版本還是使用的一個核。對于單核CPU程序、線程在執行的過程中,系統會把運作的CPU強行切給另一個程序或線程。

       有人如果看過其他人的部落格、書,都會把goroutine翻譯為協程,所謂協程就是使用者态的線程,可以這樣了解:”一個協程在執行時,系統不會強行切換時間片“。即一個goroutine在執行的過程中,Go語言會讓這個goroutine瘋狂地執行,直到它運作完為止,再讓另外一個gorouinte運作,是以從結果來看運作順序是固定的。

如果利用多核?

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

         // "time"

        "runtime"   // 引入runtime包

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

多運作幾次,結果如下:

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

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