GOLANG中的併發

golang的一大特色是對併發的支持較好,golang的併發是經過goroutine來實現的。顧名思義,goroutine就是golang實現的協程。git

咱們說併發,能夠是線程的併發,也能夠是協程的併發,協程相對於線程的優勢是協程比線程更輕量級,所以併發度能夠更高。拿goroutine來講,一個go進程包含數千個goroutine同時在跑。github

channel

golang中,每一個goroutine是獨立的,多個goroutine爲了共同完成一個任務,須要有必定的通訊機制。golang

好比說任務T由兩個協程A、B共同完成,且A、B之間存在依賴關係,協程B依賴於協程A的執行結果,也就是隻有等協程A執行完以後,協程B才能開始執行。編程

golang中協程之間的通訊是經過channel來完成的。併發

能夠把channel理解成一個管道(pipe),數據從管道的一端流進,從另外一端流出。channel的語義是,當咱們從管道中讀數據時,讀操做會一直block直到有數據流入管道;一樣的,當咱們寫數據到管道中時,寫操做一直block直到管道中的數據被讀走。ui

unbuffered channel

unbuffered channel的語義是:當咱們從管道中讀數據時,讀操做會一直block直到有數據流入管道;一樣的,當咱們寫數據到管道中時,寫操做一直block直到管道中的數據被讀走。spa

下面的case我想在程序退出以前在屏幕上輸出hello world,爲了實現這點,我使用了done這個類型爲chan bool的channel變量。線程

package main

import (
    "fmt"
    "time"
)
// 接收 bool 的 cahnnel
func hello(done chan bool) {
    fmt.Println("hello world")
    time.Sleep(4 * time.Second)
    done <- true
}
func main() {
  //用 make 創建一個不為 nil 的 channel
    done := make(chan bool)
    fmt.Println("Main going to call hello go goroutine")
    go hello(done)
    <- done // 管道讀操做一直block,直到 hello goroutine執行並往管道中寫數據,註釋掉此行,main goroutine會一直執行到結束,hello goroutine不會被調度
    fmt.Println("Main received data")
}

複製代碼

buffered channel

使用make建立channel的時候,除了類型以外,還能夠指定另一個參數capacity,capacity指定了channel的buffer長度,這種channel稱之爲buffered channel,capacity默認爲0。code

相似地,buffered channel的語義也很好理解:當buffer滿了,繼續寫就會被block;當buffer空了,繼續讀就會被block。server

package main

import (
    "fmt"
    "time"
)

func write(ch chan int) {
    for i := 0; i < 5; i++ {
        ch <- i
        fmt.Println("successfully wrote", i, "to ch")
    }
    close(ch)
}
func main() {
    ch := make(chan int, 2)
    go write(ch)
    time.Sleep(2 * time.Second)
    for v := range ch {
        fmt.Println("read value", v,"from ch")
        time.Sleep(2 * time.Second)

    }
}

/* successfully wrote 0 to ch successfully wrote 1 to ch read value 0 from ch successfully wrote 2 to ch read value 1 from ch successfully wrote 3 to ch read value 2 from ch successfully wrote 4 to ch read value 3 from ch read value 4 from ch */
複製代碼

mutex

mutex其實是一種鎖機制,確保在任一時間點,只有一個goroutine可以進入到臨界區(critical section),進而防止競爭條件(race condition)的發生。

下面的case中,啓動1000個goroutine來讓x自增1000次,每次運行的結果可能都不必定,x會小於等於1000。 這是由於x的自增操做不是原子的,某一時刻,goroutine1讀到x的值爲10,+1以後爲11,可是尚未寫入主存,此時發生協程切換,goroutine2開始運行,goroutine2從主存讀到x依然爲10,+1以後爲11,接下來,goroutine1和goroutine2把個字結果寫回主存(無論前後順序),x的值更新爲11,出現了兩次自增操做只實現了+1的效果。(local運行)

package main

import (
  "fmt"
  "sync"
)

var x = 0

func increment(wg *sync.WaitGroup, m *sync.Mutex) {
  x = x + 1
  wg.Done()
}
func main() {
  var w sync.WaitGroup
  for i := 0; i < 1000; i++ {
      w.Add(1)
      go increment(&w, &m) // 這裡必定要用 address
  }
  w.Wait()
  fmt.Println("final value of x", x)
}

複製代碼

要解決這個問題很簡單,每次執行自增操做以前先加鎖,執行完以後再釋放鎖,以此來保證自增操做的原子性。下面的case無論運行多少次,每次運行x的值都會是1000,也就是說程序的運行結果是肯定的。

package main

import (
  "fmt"
  "sync"
)

var x = 0

func increment(wg *sync.WaitGroup, m *sync.Mutex) {
  m.Lock()
  x = x + 1
  m.Unlock()
  wg.Done()
}
func main() {
  var w sync.WaitGroup
  var m sync.Mutex
  for i := 0; i < 1000; i++ {
      w.Add(1)
      go increment(&w, &m) // 這裡必定要用 address
  }
  w.Wait()
  fmt.Println("final value of x", x)
}

複製代碼

特別的,咱們還可使用channel來實現mutex的功能。

package main

import (
  "fmt"
  "sync"
)

var x = 0

func increment(wg *sync.WaitGroup, m chan int) {
  m <- 1
  x = x + 1
  <- m
  wg.Done()
}
func main() {
  var w sync.WaitGroup
  m := make(chan int, 1)
  for i := 0; i < 1000; i++ {
      w.Add(1)
      go increment(&w, m) // 這裡必定要用 address
  }
  w.Wait()
  fmt.Println("final value of x", x)
}

複製代碼

WaitGroup

除了channel和mutex以外,golang還提供了WaitGroup和Select來實現併發。

WaitGroup本質上是一個counter,只有counter=1的時候纔會繼續下一步。通常咱們使用WaitGroup來實現語義:當一組goroutine都執行完成的時候,才繼續下一步。

在下面的case中,執行一個goroutine以前counter先加1,goroutine執行完退出以前,counter減1,這就保證了只有在全部的goroutine都完成以後,纔會繼續執行main goroutine。

package main

import (
    "fmt"
    "sync"
    "time"
)

func process(i int, wg *sync.WaitGroup) {
    fmt.Println("started Goroutine ", i)
    time.Sleep(2 * time.Second)
    fmt.Printf("Goroutine %d ended\n", i)
    wg.Done() // -1
}

func main() {
    no := 3
    var wg sync.WaitGroup
    for i := 0; i < no; i++ {
        wg.Add(1) // + 1
        go process(i, &wg) // wg 必定要用 pointer,不然每一個 goroutine 都會有各自的 WaitGroup
    }
    wg.Wait() // =0 才繼續下一步
    fmt.Println("All go routines finished executing")
}
複製代碼

Select

select的語法跟switch的語法很是相似,用來實現以下語義:當一組協程中的全部協程都處於block時則block,當這組協程中有一個協程ready時,選擇這個協程執行,當一組協程裏面有多個協程ready時,隨機選一個執行。

package main

import (
  "fmt"
  "time"
)

func server1(ch chan string) {
  time.Sleep(6 * time.Second)
  ch <- "from server1"
}
func server2(ch chan string) {
  time.Sleep(3 * time.Second)
  ch <- "from server2"

}
func main() {
  output1 := make(chan string)
  output2 := make(chan string)
  go server1(output1)
  go server2(output2)

  // 等待到其中一個 channel 回來,就執行,若是都有就會隨機
  select {
  case s1 := <-output1:
      fmt.Println(s1)
  case s2 := <-output2:
      fmt.Println(s2)
  }
}
複製代碼

總結以及下篇展望

本篇介紹了golang中關於併發編程的幾個關鍵概念。golang的好處是從golang出發能夠很清楚搞清楚併發中的不少關鍵概念。

下篇介紹併發編程中的關鍵概念,它們之間的聯繫,以及這些關鍵概念在golang中的實現。

  1. critical section(臨界區)、(race condition)競爭條件
  2. 同步原語、互斥變量、條件變量、信號量、鎖

參考

相關文章
相關標籤/搜索