面試官:實現協程同步有哪些方式?

「第9期」 距離大叔的80期小目標還有71期,今天大叔要跟你們分享的內容是 —— golang中實現協程同步的幾種方式,沒錯,依舊是基礎,夯實基礎永遠都不會過期,另外是否是以爲這個題目在面試中比較熟悉呢?接下來一塊兒來了解一下吧。git

爲何要作同步

在進入正題前,咱們先習慣性地摸着良心問問自(ji)己 (ji) 個:爲何要作同步處理?github

假設如今有多個協程併發訪問操做同一塊內存中的數據,那麼可能上一納秒第一個協程剛把數據從寄存器拷貝到內存,第二個協程立刻又把此數據用它修改的值給覆蓋了,這樣共享數據變量會亂套。golang

舉個栗子:web

package main
 import(  "fmt"  "time" )  var share_cnt uint64 = 0  func incrShareCnt() {  for i:=0; i < 10000; i++ {  share_cnt++  } }  func main() {   for i:=0; i < 2; i++ {  go incrShareCnt()  }   time.Sleep(10*time.Second)   fmt.Println(share_cnt) } 複製代碼

上面代碼用2個協程序併發各自增一個全局變量1000000 次,咱們來看一下打印輸出的結果:面試

dashu@dashu > /data1/htdocs/go_practice > go run test.go
1014184 dashu@dashu > /data1/htdocs/go_practice > go run test.go 1026029 dashu@dashu > /data1/htdocs/go_practice > go run test.go 19630 ... 複製代碼

從打印結果咱們能夠看到,雖然代碼中咱們對一個全局變量自增了20000次,可是沒有一次打印輸出20000的結果,緣由就是由於協程間共享數據時發生了數據覆蓋。實際上面的代碼無聊sleep多就久都不會打印輸出20000。markdown

協程同步方法

那麼,如何才能讓數據在goroutine之間達到同步呢?下面跟你們分享如下三種數據同步的方式:併發

  • time.Sleep
  • channel
  • sync.WaitGroup

time.Sleep

爲何sleep能夠用來實現數據同步呢?咱們看個栗子:app

func main()  {
 go func() {  fmt.Println("goroutine1")  }()   go func() {  fmt.Println("goroutine2")  }() } 複製代碼

執行上面那段代碼你會發現沒有任何輸出,緣由是:主協程在兩個協程還沒執行完就已經結束了,而主協程結束時會結束全部其餘協程, 因此致使代碼運行的結果什麼都沒有。函數

咱們在主協程結束前 sleep 一段時間就 可能出現 告終果:oop

func main()  {
 go func() {  fmt.Println("goroutine1")  }()   go func() {  fmt.Println("goroutine2")  }()   time.Sleep(time.Second) } 複製代碼

打印輸出:

goroutine1
goroutine2 複製代碼

爲何上面我要說 「可能會出現」 呢?上面代碼中咱們設置了睡眠時間爲1s,因爲協程的處理邏輯比較簡單,因此能正常打印輸出上面結果;若是我這兩個協程裏面執行了很複雜的邏輯操做(時間大於 1s),那麼就會發現依舊也是無結果打印出來的。

因此又一個問題來了:咱們沒法肯定須要睡眠多久

看來這sleep着實不靠譜,有沒有什麼辦法來代替sleep呢?答案確定是有的,咱們來看第二種方法。

channel(信道)

channel是如何實現goroutine同步的呢?咱們再看個典型的栗子:channel實現簡單的生產者和消費者

package main
 import (  "fmt"  "time" )  func producer(ch chan int, count int) {  for i := 1; i <= count; i++ {  fmt.Println("大媽作第", i, "個麪包")  ch <- i   // 睡眠一下,可讓整個生產消費看得更清晰點  time.Sleep(time.Second * time.Duration(1))  } }  func consumer(ch chan int, count int) {  for v := range ch {  fmt.Println("大叔吃了第", v, "個麪包")  count--  if count == 0 {  fmt.Println("沒麪包了,大叔也飽了")  close(ch)  }  } }  func main() {  ch := make(chan int)  count := 5  go producer(ch, count)  consumer(ch, count) } 複製代碼

上面代碼中,咱們另外起了個 goroutine 讓大媽來生產5個麪包(實際就是往channel中寫數據),主 goroutine 讓大叔不斷吃麪包(從channel中讀數據)。咱們來看一下輸出結果:

大媽作第 1 個麪包
大叔吃了第 1 個麪包 大媽作第 2 個麪包 大叔吃了第 2 個麪包 大媽作第 3 個麪包 大叔吃了第 3 個麪包 大媽作第 4 個麪包 大叔吃了第 4 個麪包 大媽作第 5 個麪包 大叔吃了第 5 個麪包 沒麪包了,大叔也飽了 複製代碼

從輸出結果咱們能夠看到,大媽一共作了5個麪包,大叔一共吃了5個麪包,同步上了!

Tip

上面代碼,咱們用 for-range 來讀取 channel的數據,for-range 是一個頗有特點的語句,有如下特色:

  • 若是 channel 已經被關閉,它仍是會繼續執行,直到全部值被取完,而後退出執行
  • 若是通道沒有關閉,可是channel沒有可讀取的數據,它則會阻塞在 range 這句位置,直到被喚醒。
  • 若是 channel 是 nil,那麼一樣符合咱們上面說的的原則,讀取會被阻塞,也就是會一直阻塞在 range 位置。

咱們來驗證一下,咱們把上面代碼中的 close(ch) 移到主協程中試試:

package main
 import (  "fmt"  "time" )  func producer(ch chan int, count int) {  for i := 1; i <= count; i++ {  fmt.Println("大媽作第", i, "個麪包")  ch <- i   // 睡眠一下,可讓整個生產消費看得更清晰點  time.Sleep(time.Second * time.Duration(1))  } }  func consumer(ch chan int, count int) {  for v := range ch {  fmt.Println("大叔吃了第", v, "個麪包")  count--  if count == 0 {  fmt.Println("沒麪包了,大叔也飽了")  }  } }  func main() {  ch := make(chan int)  count := 5  go producer(ch, count)  consumer(ch, count)  close(ch) } 複製代碼

打印輸出:

大媽作第 1 個麪包
大叔吃了第 1 個麪包 大媽作第 2 個麪包 大叔吃了第 2 個麪包 大媽作第 3 個麪包 大叔吃了第 3 個麪包 大媽作第 4 個麪包 大叔吃了第 4 個麪包 大媽作第 5 個麪包 大叔吃了第 5 個麪包 沒麪包了,大叔也飽了 fatal error: all goroutines are asleep - deadlock!  goroutine 1 [chan receive]: main.consumer(0xc00008c060, 0x0)  /data1/htdocs/go_project/src/github.com/cnyygj/go_practice/test.go:19 +0x5f main.main()  /data1/htdocs/go_project/src/github.com/cnyygj/go_practice/test.go:32 +0x7c exit status 2 複製代碼

果真阻塞掉了,最終造成了死鎖,拋出異常了。

sync.WaitGroup

若是你覺的上面兩種方法還不過癮,接下來咱們再看個方法:sync.WaitGroup

WaitGroup 內部實現了一個計數器,用來記錄未完成的操做個數,它提供了三個方法:

  • Add() 用來添加計數
  • Done() 用來在操做結束時調用,使計數減一 【我不會告訴你 Done() 方法的實現其實就是調用 Add(-1)】
  • Wait() 用來等待全部的操做結束,即計數變爲 0,該函數會在計數不爲 0 時等待,在計數爲 0 時當即返回

仍是看栗子:

func main()  {
 var wg sync.WaitGroup  wg.Add(2) // 由於有兩個動做,因此增長2個計數   go func() {  fmt.Println("Goroutine 1")  wg.Done() // 操做完成,減小一個計數  }()   go func() {  fmt.Println("Goroutine 2")  wg.Done() // 操做完成,減小一個計數  }()   wg.Wait() // 等待,直到計數爲0  } 複製代碼

打印輸出:

Goroutine 1
Goroutine 2 複製代碼

以上就是今天要跟你們分享的內容,歡迎留言交流~

關注公衆號 「大叔說碼」,獲取更多幹貨,下期見~

相關文章
相關標籤/搜索