GO語言基礎之並發concurrency

來源:互聯網
上載者:User

標籤:開發人員   div   關係   單位   回收   引用   並發   類型   必須   

並發Concurrency

  很多人都是衝著 Go 大肆宣揚的高並發而忍不住躍躍欲試,但其實從源碼的解析來看,goroutine 只是由官方實現的超級“線程池”而已。不過話說回來,每個執行個體 4~5KB的棧記憶體佔用和由於實現機制而大幅減少的建立和銷毀開銷,是製造 Go 號稱的高並發的根本原因。另外,goroutine 的簡單易用,也在語言層面上給予了開發人員巨大的遍曆。

  高並發當中一定要注意:並發可不是並行。

  並發主要由切換時間片來實現“同時”運行,而並行則是直接利用多核實現多線程的運行,但 Go 可以設定使用核心數,以發揮多核電腦的處理能力。

  goroutine 奉行通過通訊來共用記憶體,而不是共用記憶體來通訊。Go 語言主要是通過 Channe 技術通訊來實現記憶體的共用的,因為 channel 是一個通道,Go 是通過通道來通訊進行記憶體資料的共用。

  對於初學者,goroutine直接理解成為線程就可以了。當對一個函數調用go,啟動一個goroutine的時候,就相當於起來一個線程,執行這個函數。

  實際上,一個goroutine並不相當於一個線程,goroutine的出現正是為了替代原來的線程概念成為最小的調度單位。一旦運行goroutine時,先去當先線程尋找,如果線程阻塞了,則被分配到閒置線程,如果沒有閒置線程,那麼就會建立一個線程。注意的是,當goroutine執行完畢後,線程不會回收推出,而是成為了閒置線程。

讓我們先來看一個最簡單的 goroutine 案例:

package mainimport (    "fmt"    "time")func main() {    //啟用一個goroutine    go GoRun()    //這裡加一個休眠是因為主線程已啟動就執行完畢消亡來,子線程還來不及執行    time.Sleep(2 * time.Second)}func GoRun() {    fmt.Println("Go Go Go!!!")}

運行結果:

1 Go Go Go!!!

Channel

1. Channel 是 goroutine 溝通的橋樑,大都是阻塞同步的

2. 它是通過 make 建立,close 關閉

3. Channel 是參考型別

4. 可以使用 for range 來迭代,不斷操作 channel

5. 可以設定單向 或 雙向通道

6. 可以設定緩衝大小,在未被填滿前不會發生阻塞,即它是非同步

那麼針對上溯代碼我們不使用休眠,而使用 Channel 來實現我們想要的效果:

channel的意思用白話可以這麼理解:主線程告訴大家你開goroutine可以,但是我在我的主線程開了一個管道,你做完了你要做的事情之後,往管道裡面塞個東西告訴我你已經完成了。

package mainimport (    "fmt")func main() {    //聲明建立一個通道,儲存類型為bool型    c := make(chan bool)    //啟用一個goroutine,使用的是匿名方法方式    go func() {        fmt.Println("Go Go Go!!!")        c <- true  //向 channel 中存入一個值    }()    //當程式執行完畢之後再從通道中取出剛才賦的值    <- c    /**    主線程啟動了一個匿名子線程後就執行到了:<-c , 到達這裡主線程就被阻塞了。只有當子線程向通道放入值後主線程阻塞才會被釋放    其實這個就是完成了訊息的發送     */}

上溯代碼可以修改為使用 for range 來進行訊息的發送:

package mainimport (    "fmt")func main() {    //聲明建立一個通道,儲存類型為bool型,這裡設定的channel就是雙向通道,既可以存也可以取    c := make(chan bool)    //啟用一個goroutine,使用的是匿名方法方式    go func() {        fmt.Println("Go Go Go!!!")        c <- true  //向 channel 中存入一個值        close(c)  //切記如果使用for range來進行取值的時候需要在某個地方進行關閉,否則會發生死結    }()    //從通道中迴圈取出剛才賦的值    for v := range c {        fmt.Println(v)    }}

  從以上代碼可以看出,一般使用的 Channel 都是雙向通道的,即:既可以取又可以存。那單向通道一般用於什麼情境下呢?

  單向通道又分為兩種,一種是只能讀取,一種是只能存放,一般用於參數類型傳遞使用。例如有個方法返回一個Channel類型,一般要求操作只能從這裡取,那麼此時它的用途就是只能存放類型,如果此時你不小心存資料,此時會發生panic 導致程式奔潰發生異常。那麼讀取類型的Channel同理。這樣做其實也是為了程式的安全性與健壯性,防止一些誤操作。

  這裡還有一個知識點,就是有緩衝的channel 和 無緩衝的channel的區別?

    make(chan bool, 1) 表示帶有一個緩衝大小的緩衝channel

    make(chan bool) 或 make(chan bool, 0) 表示一個無緩衝的channel

    無緩衝channel是阻塞的即同步的,而有緩衝channel是非同步。怎麼說?比如

    c1:=make(chan int)         無緩衝

    c2:=make(chan int,1)      有緩衝

    c1 <- 1  //往無緩衝通道放入資料 1                         

    無緩衝的 不僅僅是 向 c1 通道放 1 而且一定要有別的線程 <- c1 接手了這個參數,那麼 c1 <- 1 才會繼續下去,要不然就一直阻塞著

    而 c2 <- 1 則不會阻塞,因為緩衝大小是1 只有當放第二個值的時候第一個還沒被人拿走,這時候才會阻塞。

  打個比喻

    無緩衝的  就是一個送信人去你家門口送信 ,你不在家 他不走,你一定要接下信,他才會走。

    無緩衝保證信能到你手上

    有緩衝的 就是一個送信人去你家仍到你家的信箱 轉身就走 ,除非你的信箱滿了 他必須等信箱空下來。

    有緩衝的 保證 信能進你家的郵箱

 那如果在多線程環境下,多個線程並發搶佔會使得列印不是按照順序來,那麼我們如何確保子線程全部結束完之後主線程再停止呢?主要有兩種方式:

第一種:使用阻塞channel

package mainimport (    "fmt"    "runtime")func main() {    fmt.Println("當前系統核心數:", runtime.NumCPU())    runtime.GOMAXPROCS(runtime.NumCPU()) //設定當前程式執行使用的並發數    //定義一個阻塞channel    c := make(chan bool)    //這裡啟動10個線程運行    for i :=0; i < 10; i++ {        go goRun(c, i)    }    //我們知道一共有10次迴圈,那麼在這裡就取10次,那麼子線程goRun只有都執行完了主線程取才能完畢,因為這裡也迴圈取10次,不夠的話會被阻塞    for i := 0; i < 10; i++ {        <- c    }}func goRun(c chan bool, index int) {    a := 1    //迴圈疊加1千萬次並返回最終結果    for i := 0; i < 10000000; i++ {        a += i    }    fmt.Println("線程式號:", index, a)   //往阻塞隊列插入內容    c <- true}

列印結果:

1234567891011 當前系統核心數: 4線程式號: 9 49999995000001線程式號: 5 49999995000001線程式號: 2 49999995000001線程式號: 0 49999995000001線程式號: 6 49999995000001線程式號: 1 49999995000001線程式號: 3 49999995000001線程式號: 7 49999995000001線程式號: 8 49999995000001線程式號: 4 49999995000001

從列印結果可以看出,多線程環境下運行代碼列印和順序沒有關係,由 CPU 調度自己決定,多運行幾次列印結果一定不會一樣,就是這個道理。

第二種:使用同步機制

package mainimport (    "fmt"    "runtime"    "sync")func main() {    fmt.Println("當前系統核心數:", runtime.NumCPU())    runtime.GOMAXPROCS(runtime.NumCPU()) //設定當前程式執行使用的並發數    /**    waitGroup即工作群組,它的最要作用就是用來添加需要工作的任務,沒完成一次任務就標記一次Done,這樣工作群組的待完成量會隨之減1    那麼主線程就是來判斷工作群組內是否還有未完成任務,當沒有未完成當任務之後主線程就可以結束運行,從而實現了與阻塞隊列類似的同步功能    這裡建立了一個空的waitGroup(工作群組)     */    wg := sync.WaitGroup{}    wg.Add(10)  //添加10個任務到工作群組中    //這裡啟動10個線程運行    for i :=0; i < 10; i++ {        go goRun(&wg, i)    }    wg.Wait()}/**這裡需要傳入參考型別不能傳入值拷貝,因為在子線程中是需要執行Done操作,類似與我們修改結構體中的int變數主詞遞減,如果是只拷貝的話是不會影響原類型內的資料這樣就會發生死迴圈導致死結程式奔潰,報錯異常為:fatal error: all goroutines are asleep - deadlock! */func goRun(wg *sync.WaitGroup, index int) {    a := 1    //迴圈疊加1千萬次並返回最終結果    for i := 0; i < 10000000; i++ {        a += i    }    fmt.Println("線程式號:", index, a)    wg.Done()}

列印結果:

1234567891011 當前系統核心數: 4線程式號: 1 49999995000001線程式號: 5 49999995000001線程式號: 0 49999995000001線程式號: 9 49999995000001線程式號: 4 49999995000001線程式號: 3 49999995000001線程式號: 2 49999995000001線程式號: 6 49999995000001線程式號: 8 49999995000001線程式號: 7 49999995000001

  以上所有講解到的都是基於一個 channel 來說的,那麼當我們有多個 channel 的時候又該怎麼處理呢?

  Go 語言為我們提供了一種結構名為:Select,它和 switch 是非常相似的,switch 主要用於普通類型做判斷的,而 select 主要是針對多個 channel 來進行判斷的。

Select

1. 可處理一個或多個 channel 的發送與接收

2. 同時有多個可用的 channel 時,可以按隨機順序處理

3. 可以使用空的 select 來阻塞 main 函數

4. 它還可以設定逾時時間

案例一:用多個 channel 來接收資料:

package mainimport (    "fmt")/**資料接收處理 */func main() {    //批量初始化channel    c1, c2 := make(chan int), make(chan string)    //建立一個啟動goroutine的匿名函數    go func() {        /**        建立一個無限迴圈語句,使用select進行處理        我們一般都是使用這種方式來處理不斷的訊息發送和處理         */        for {            select {            case v, ok := <- c1:                if !ok {                    break                }                fmt.Println("c1:", v)            case v, ok := <- c2:                if !ok {                    break                }                fmt.Println("c2:", v)            }        }    }()    c1 <- 1    c2 <- "liang"    c1 <- 2    c2 <- "xuli"    //關閉channel    close(c1)    close(c2)}

列印結果:

1234 c1: 1c2: liangc1: 2c2: xuli

 案例二:用多個 channel 來發送資料:

package mainimport (    "fmt")/**資料接收處理,這裡實現隨機接收0、1 數字並列印 */func main() {    c := make(chan int)    num := 0    //建立一個啟動goroutine的匿名函數    go func() {        for v := range c {            num++            if num & 15 == 0 {                fmt.Println()            }            fmt.Print(v, " ")        }    }()    for {        select {        case c <- 0:        case c <- 1:        }    }}

列印結果:(只是粘貼了其中一部分)

1234567891011121314151617 1 1 0 1 1 0 0 0 0 1 0 1 0 0 1 00 1 0 1 1 0 1 1 0 0 1 1 1 0 0 11 1 1 1 0 0 1 1 0 0 0 0 0 1 0 10 1 1 0 0 0 1 1 1 0 0 0 1 1 0 01 1 1 0 0 0 0 0 1 0 1 1 1 1 1 10 0 1 0 0 0 0 1 0 1 1 0 1 1 1 01 1 1 0 0 0 1 1 1 0 0 0 1 0 0 11 0 1 1 1 1 0 0 1 0 0 1 1 1 1 11 1 0 0 0 0 0 1 1 1 0 1 1 0 1 11 1 0 0 0 0 1 0 0 1 0 1 0 0 1 10 0 0 1 1 1 1 1 0 0 0 1 0 0 0 10 1 1 0 1 0 1 0 1 0 0 1 1 0 0 00 1 0 0 0 1 0 0 0 1 1 0 0 0 1 11 1 0 1 1 1 1 0 0 0 1 0 0 0 1 10 1 1 0 0 1 1 0 1 0 1 0 0 0 0 10 1 1 0 0 0 1 1 0 1 0 1 0 0 0 00 0 0 1 0 0 0 1 1 1 1 1 1 1 1 0

 案例三:用 channel 設定逾時時間: 

package mainimport (    "fmt"    "time")/**select的逾時應用 */func main() {    c := make(chan bool)    select {    case v := <- c :        fmt.Println(v)    case <- time.After(3 * time.Second):        fmt.Println("TimeOut!!!")    }}

列印結果:

1 TimeOut!!!

GO語言基礎之並發concurrency

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.