Go基礎學習六之併發concurrency

Corwien發表於2019-02-16

Go程式語言:支援併發、垃圾回收的編譯型系統級程式語言!Go語言從語言層面上就支援了併發,這與其他語言大不一樣,不像以前我們要用Thread庫 來新建執行緒,還要用執行緒安全的佇列庫來共享資料。

一、併發concurrency

1.基本概念

Go能處理高併發的根本原因在於執行go協程只需極少的棧記憶體(大概4~5KB),並且能根據需要動態增長和縮減佔用的資源。

Go的高併發其實是由goroutine實現的,goroutine是由官方實現的超級“執行緒池”。

簡單而言,goroutine就是一段程式碼,一個函式入口,以及在堆上為其分配的一個堆疊。所以它非常廉價,我們可以很輕鬆的建立上萬個goroutine,但它們並不是被作業系統所排程執行,而是通過系統的執行緒來多路派遣這些函式的執行,使得每個用go關鍵字執行的函式可以執行成為一個單位協程。當一個協程阻塞的時候,排程器就會自動把其他協程安排到另外的執行緒中去執行,從而實現了程式無等待並行化執行。而且排程的開銷非常小,一顆CPU排程的規模不下於每秒百萬次,這使得在程式中能夠建立大量的goroutine,實現高併發的同時,依舊能保持高效能。

Goroutine 奉行通過通訊來共享記憶體,而不是共享記憶體來通訊。

2.區別併發和並行

這裡涉及到一個問題,很多同學搞不清楚併發與並行的區別,這裡我根據我根據知乎上這個問題某位網友的例子,我覺得很好:

  • 你吃飯吃到一半,電話來了,你一直到吃完了以後才去接,這就說明你不支援併發也不支援並行
  • 你吃飯吃到一半,電話來了,你停了下來接了電話,接完後繼續吃飯,這說明你支援併發
  • 你吃飯吃到一半,電話來了,你一邊打電話一邊吃飯,這說明你支援並行

併發:你有處理多個任務的能力,不一定同時(一個CPU輪流)

並行:有同時處理多個任務的能力(多個CPU同時)

併發和並行都可以是很多個執行緒,就看這些執行緒能不能同時被(多個)CPU執行,可以說明是並行,併發是多個執行緒被一個CPU輪流切換著執行。

併發不是並行:Concurrency Is Not Parallelism
併發主要由切換時間片來實現“同時”執行,而並行則是直接利用多核實現多執行緒的執行,但Go可以設定使用核數,以發揮多核計算機的能力。

併發時,由於cpu執行的太快了,不停地來回切換,讓人以為是同時進行。
併發比並行更優秀,充分的利用了CPU。

3.通道Channel

  • Channelgoroutine 溝通的橋樑,大都是阻塞同步的
  • 通過 make 建立,close 關閉
  • Channel 是引用型別
  • 可以使用 for range 來迭代不斷操作channel
  • 可以設定單向或雙向通道
  • 可以設定快取大小,在未被填滿前不會發生阻塞

4.Select

  • 可處理一個或多個channel的傳送與接收
  • 同時有多個可用的channel時按隨機排序處理
  • 可用空的 select 來阻塞 main 函式
  • 可設定超時

二、併發示例

1.未使用channel關鍵字

package main


import (
    "fmt"
    "time"
)  

func main() {
    go Go()      // 使用go關鍵字就可執行goroutine
    time.Sleep(2 * time.Second)
    fmt.Println("Hey ...!")
}

func Go() {
    fmt.Println("Go ...!")
    
}

列印:

➜  src go run myfirstgo/concurrency.go
Go ...!
Hey ...!

2.使用channel關鍵字

package main


import (
    "fmt"
)  

func main() {

    // 建立channel
    c := make(chan bool)

    // 執行到此處會阻塞
    go func() {
        fmt.Println("Go ...!")
        c <- true
    }()

    // 將true的東西存到channel當中,然後讀出來結束執行,即通知main函式,我這裡執行完成了,可以結束了
    <-c
    
    fmt.Println("Hey ...!")
}

列印:

➜  src go run myfirstgo/concurrency.go
Go ...!
Hey ...!

3.range示例

func main() {

    // 建立channel
    c := make(chan bool)

    // 執行到此處會阻塞
    go func() {
        fmt.Println("Go ...!")
        c <- true
        close(c)
    }()

   for v := range c {
         fmt.Println(v)
   }
    
    fmt.Println("Hey ...!")
}

列印:

➜  src go run myfirstgo/concurrency.go
Go ...!
true
Hey ...!

4.多核

package main


import (
    "fmt"
    "runtime"
)  

// 當使用單執行緒執行時,會按部就班,按照順序1,2,3,4執行下去
// 當使用多個CPU核數時,任務分配是不定的,

func main() {

    // 使用多核
    runtime.GOMAXPROCS(runtime.NumCPU())
    c := make(chan bool, 10)
    for i := 0; i < 10; i++ {
        go Go(c, i)
    }
    <-c
}

func Go(c chan bool, index int) {
    a := 1
    for i := 0; i < 100000000; i++ {
        a += i
    }

    fmt.Println(index, a)

    if index == 9 {
        c <- true
    }
}

執行多次的執行結果:

➜  src go run myfirstgo/concurrency.go
3 4999999950000001
0 4999999950000001
1 4999999950000001
9 4999999950000001
➜  src go run myfirstgo/concurrency.go
2 4999999950000001
1 4999999950000001
3 4999999950000001
9 4999999950000001
➜  src go run myfirstgo/concurrency.go
9 4999999950000001
➜  src go run myfirstgo/concurrency.go
2 4999999950000001
9 4999999950000001

當使用多個CPU核數(runtime.GOMAXPROCS)時,任務分配是不定的,所以會出現上邊的結果。

這裡有兩種解決方案:
第一種:
設定一個快取長度為10的channel

package main


import (
    "fmt"
    "runtime"
)  

// 當使用單執行緒執行時,會按部就班,按照順序1,2,3,4執行下去
// 當使用多個CPU核數時,任務分配是不定的,

func main() {

    // 使用多核
    runtime.GOMAXPROCS(runtime.NumCPU())
    c := make(chan bool, 10)
    for i := 0; i < 10; i++ {
        go Go(c, i)
    }

    // 設定一個快取長度為10 的channel
    for i := 0; i < 10; i++ {
        <-c
    }

    
}

func Go(c chan bool, index int) {
    a := 1
    for i := 0; i < 100000000; i++ {
        a += i
    }

    fmt.Println(index, a)
    c <- true

}

列印:

➜  src go run myfirstgo/concurrency.go
1 4999999950000001
2 4999999950000001
9 4999999950000001
3 4999999950000001
7 4999999950000001
0 4999999950000001
8 4999999950000001
6 4999999950000001
4 4999999950000001
5 4999999950000001
➜  src go run myfirstgo/concurrency.go
0 4999999950000001
9 4999999950000001
5 4999999950000001
1 4999999950000001
7 4999999950000001
2 4999999950000001
8 4999999950000001
3 4999999950000001
6 4999999950000001
4 4999999950000001

第二種:
不是通過channel解決的,而是通過sync包解決的,它有一個waitGroup,可以建立一個任務組,可以新增任務,每完成一個任務,就標記完成Done,main函式的主要作用就是判斷是否所有的任務都完成了,如果都完成了,則退出程式。

package main

import (
    "fmt"
    "runtime"
    "sync"
)  

// 當使用單執行緒執行時,會按部就班,按照順序1,2,3,4執行下去
// 當使用多個CPU核數時,任務分配是不定的,
func main() {

    // 使用多核
    runtime.GOMAXPROCS(runtime.NumCPU())

    // sync 
    wg := sync.WaitGroup{}
    wg.Add(10)

    for i := 0; i < 10; i++ {
        go Go(&wg, i)
    }

    wg.Wait()

    
}

func Go(wg *sync.WaitGroup, index int) {
    a := 1
    for i := 0; i < 100000000; i++ {
        a += i
    }

    fmt.Println(index, a)
    
    wg.Done()
}

列印:

➜  src go run myfirstgo/concurrency.go
1 4999999950000001
5 4999999950000001
7 4999999950000001
9 4999999950000001
6 4999999950000001
8 4999999950000001
0 4999999950000001
2 4999999950000001
3 4999999950000001
4 4999999950000001
➜  src go run myfirstgo/concurrency.go
1 4999999950000001
9 4999999950000001
2 4999999950000001
3 4999999950000001
4 4999999950000001
5 4999999950000001
0 4999999950000001
6 4999999950000001
7 4999999950000001
8 4999999950000001

相關文章:
golang語言併發與並行——goroutine和channel的詳細理解(一)
golang的goroutine排程機制

相關文章