GO通道和 sync 包的分享

[TOC]shell

GO通道和 sync 包的分享

咱們一塊兒回顧一下上次分享的內容:安全

  • GO協程同步若不作限制的話,會產生數據競態的問題
  • 咱們用鎖的方式來解決如上問題,根據使用場景選擇使用互斥鎖 和 讀寫鎖
  • 比使用鎖更好的方式是原子操做,可是使用go的 sync/atomic須要當心使用,由於涉及內存

要是對GO的鎖和原子操做還感興趣的話,歡迎查看文章GO的鎖和原子操做分享數據結構

上次咱們分享到鎖和原子操做,均可以保證共享數據的讀寫閉包

但是,他們仍是會影響性能,不過,Go 爲開發這提供了 通道 這個神器併發

今天咱們來分享一下Go中推薦使用的其餘同步方法,通道和 sync 包ide

通道是什麼?

是一種特殊的類型,是鏈接併發goroutine的管道函數

channel 通道是可讓一個 goroutine 協程發送特定值到另外一個 goroutine 協程的通訊機制高併發

通道像一個傳送帶或者隊列,老是遵循先入先出(First In First Out)的規則,保證收發數據的順序,這一點和管道是同樣的post

一個協程從通道的一頭放入數據,另外一個協程從通道的另外一頭讀出數據性能

每個通道都是一個具體類型的導管,聲明 channel 的時候須要爲其指定元素類型。

通道能作什麼?

控制協程的同步,讓程序有序運行

GO 中提倡 不要經過共享內存來通訊,而經過通訊來共享內存

goroutine協程 是 Go 程序併發的執行體,channel 通道就是它們之間的鏈接,他們之間的橋樑,他們的交通樞紐

通道有哪幾種?

大體可分爲以下三種:

  • 無緩衝通道
  • 有緩衝的通道
  • 單向通道

無緩衝通道

無緩衝的通道又稱爲阻塞的通道

無緩衝通道上的發送操做會阻塞,直到另外一個goroutine在該通道上執行接收操做,這時值才能發送成功

兩個 goroutine 協程將繼續執行

咱們反過來看,若是接收操做先執行,接收方的goroutine將阻塞,直到另外一個 goroutine 協程在該通道上發送一個數據

所以,無緩衝通道也被稱爲同步通道,由於咱們可使用無緩衝通道進行通訊,利用發送和接收的 goroutine 協程同步化

有緩衝的通道

仍是上述提到的,有緩衝通道,就是在初始化 / 建立通道 的 make 函數的第 2 個參數填上咱們所指望的緩衝區大小 , 例如:

ch1 := make(chan int , 4)

此時,該通道的容量爲4,發送方能夠一直向通道中發送數據,直到通道滿,且通道數據未被讀走時,發送方就會阻塞

只要通道的容量大於零,那麼該通道就是有緩衝的通道

通道的容量表示通道中能存放元素的數量

咱們可使用內置的 len函數 獲取通道內元素的數量,使用 cap函數 獲取通道的容量

單向通道

通道默認是既能夠讀有能夠寫的,可是單向通道就是要麼只能讀,要麼只能寫

  • chan <- int

是一個只能發送的通道,能夠發送可是不能接收

  • <- chan int

是一個只能接收的通道,能夠接收可是不能發送

如何建立和聲明一個通道

聲明通道

在 Go 裏面,channel是一種類型,默認就是一種引用類型

簡單解釋一下什麼是引用:

在咱們寫C++的時候,用到引用會比較多

引用,顧名思義是某一個變量或對象的別名,對引用的操做與對其所綁定的變量或對象的操做徹底等價

在C++裏面是這樣用的:

類型 &引用名=目標變量名;

聲明一個通道

var 變量名 chan 元素類型

var ch1 chan string               // 聲明一個傳遞字符串數據的通道
var ch2 chan []int                 // 聲明一個傳遞int切片數據的通道
var ch3 chan bool                  // 聲明一個傳遞布爾型數據的通道
var ch4 chan interface{}          // 聲明一個傳遞接口類型數據的通道

看,聲明一個通道就是這麼簡單

對於通道來講,關聲明瞭還不能使用,聲明的通道默認是其對應類型的零值,例如

  • int 類型 零值 就是 0
  • string 類型 零值就是個 空串
  • bool 類型 零值就是 false
  • 切片的 零值 就是 nil

咱們還須要對通道進行初始化才能夠正常使用通道哦

初始化通道

通常是使用 make 函數初始化以後才能使用通道,也能夠直接使用make函數 建立通道

例如:

ch5 := make(chan string)
ch6 := make(chan []int)
ch7 := make(chan bool)
ch8 := make(chan interface{})

make 函數的第二個參數是能夠設置緩衝的大小的,咱們來看看源碼的說明

// The make built-in function allocates and initializes an object of type
// slice, map, or chan (only). Like new, the first argument is a type, not a
// value. Unlike new, make's return type is the same as the type of its
// argument, not a pointer to it. The specification of the result depends on
// the type:
// Slice: The size specifies the length. The capacity of the slice is
// equal to its length. A second integer argument may be provided to
// specify a different capacity; it must be no smaller than the
// length. For example, make([]int, 0, 10) allocates an underlying array
// of size 10 and returns a slice of length 0 and capacity 10 that is
// backed by this underlying array.
// Map: An empty map is allocated with enough space to hold the
// specified number of elements. The size may be omitted, in which case
// a small starting size is allocated.
// Channel: The channel's buffer is initialized with the specified
// buffer capacity. If zero, or the size is omitted, the channel is
// unbuffered.
func make(t Type, size ...IntegerType) Type

若是 make 函數的第二個參數不填,那麼就默認是無緩衝的通道

如今咱們來看看如何操做 channel 通道,均可以怎麼玩

如何操做 channel

通道的操做有以下三種操做:

  • 發送(send)
  • 接收(receive)
  • 關閉(close)

對於發送和接收通道里面的數據,寫法就比較形象,使用 <- 來指向是從通道里面讀取數據,仍是從通道中發送數據

向通道發送數據

// 建立一個通道
ch := make(chan int)
// 發送數據給通道
ch <- 1

咱們看到箭頭的方向是,1 指向了 ch 通道,因此不難理解,這是將1 這個數據,放入通道中

從通道中接收數據

num := <-ch

不難看出,上述代碼是 ch 指向了一個須要初始化的變量,也就是說,從 ch 中讀出一個數據,賦值給 num

咱們從通道中讀出數據,也能夠不進行賦值,直接忽略也是能夠的,如:

<-ch

關閉通道

Go中提供了 close 函數來關閉通道

close(ch)

對於關閉通道很是須要注意,用很差直接致使程序崩潰

  • 只有在通知接收方 goroutine 協程全部的數據都發送完畢的時候才須要關閉通道
  • 通道是能夠被垃圾回收機制回收的,它和關閉文件是不同的,在結束操做以後關閉文件是必需要作的,但關閉通道不是必須的

關閉後的通道有如下 4 個特色:

  • 對一個關閉的通道再發送值就會致使 panic
  • 對一個關閉的通道進行接收會一直獲取值直到通道爲空
  • 對一個關閉的而且沒有值的通道執行接收操做會獲得對應類型的零值
  • 關閉一個已經關閉的通道會致使 panic

通道異常狀況梳理

咱們來整理一下對於通道會存在的異常:

channel 狀態 未初始化的通道(nil) 通道非空 通道是空的 通道滿了 通道未滿
接收數據 阻塞 接收數據 阻塞 接收數據 接收數據
發送數據 阻塞 發送數據 發送數據 阻塞 發送數據
關閉 panic 關閉通道成功
待數據讀取完畢後
返回零值
關閉通道成功
直接返回零值
關閉通道成功
待數據讀取完畢後
返回零值
關閉通道成功
待數據讀取完畢後
返回零值

每一種通道的DEMO實戰

無緩衝通道

func main() {
   // 建立一個無緩衝的,數據類型 爲 int 類型的通道
   ch := make(chan int)
   // 向通道中寫入 數字 1
   ch <- 1
   fmt.Println("send successfully ... ")
}

執行上述代碼咱們能夠查看到效果

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
        F:/my_channel/main.go:9 +0x45
exit status 2

出現上述報錯 deadlock 錯誤的緣由,細心的小夥伴應該可以知道爲何,我上述有提到

咱們使用 ch := make(chan int) 建立的是無緩衝的通道

無緩衝的通道只有在有接收方接收值的時候才能發送數據成功

咱們能夠想一下咱們生活中的案例同樣:

你在某東上買了一個稍微貴重一點的物品,某東快遞人員給你寄快遞的時候,打電話給你,必需要送到你的手上,否則不敢簽收,這個時候,你不方便,或者你不簽收,那麼這個快遞就是算做沒有寄送成功

所以,上述問題緣由是,建立了一個無緩衝通道,發送方一直在阻塞,通道中一直未有協程讀取數據,致使死鎖

咱們的解決辦法就是建立另一個協程,將數據從通道中讀出來便可

package main

import "fmt"

func recvData(c chan int) {
    ret := <-c
    fmt.Println("recvData successfully ... data = ", ret)
}

func main() {
    // 建立一個無緩衝的,數據類型 爲 int 類型的通道
    ch := make(chan int)
    go recvData(ch)
    // 向通道中寫入 數字 1
    ch <- 1
    fmt.Println("send successfully ... ")
}

這裏須要注意,若是 go recvData(ch) 放在了 ch <- 1 以後,那麼結果仍是同樣的死鎖,緣由仍是由於 ch <- 1 會一直阻塞,根本不會執行到 他以後的語句

實際效果

recvData successfully ... data =  1
send successfully ...

有緩衝通道

func main() {
   // 建立一個無緩衝的,數據類型 爲 int 類型的通道
   ch := make(chan int , 1)
   // 向通道中寫入 數字 1
   ch <- 1
   fmt.Println("send successfully ... ")
}

仍是一樣的案例,一樣的代碼,咱們只是把無緩衝通道,換成了有緩衝的通道, 咱們仍然不專門開協程讀取通道的數據

實際效果 , 發送成功

$$ $$

send successfully ...

由於此時通道中的緩衝是1,第一次向通道中發送數據,不會阻塞,

但是若是,在通道中數據還未讀取出去以前,又向通道中寫入數據,則此處會阻塞,

若一直沒有協程從通道中讀取數據,則結果與上述同樣,會死鎖

單向通道

package main

import "fmt"

func OnlyWriteData(out chan<- int) {
   // 單向 通道 , 只寫 不能讀
   for i := 0; i < 10; i++ {
      out <- i
   }
   close(out)
}

func CalData(out chan<- int, in <-chan int) {
   // out 單向 通道 , 只寫 不能讀
   // int 單向 通道 , 只讀 不能寫

   // 遍歷 讀取in 通道,若 in通道 數據讀取完畢,則阻塞,若in 通道關閉,則退出循環
   for i := range in {
      out <- i + i
   }
   close(out)
}
func myPrinter(in <-chan int) {
   // 遍歷 讀取in 通道,若 in通道 數據讀取完畢,則阻塞,若in 通道關閉,則退出循環
   for i := range in {
      fmt.Println(i)
   }
}

func main() {
   // 建立2 個無緩衝的通道
   ch1 := make(chan int)
   ch2 := make(chan int)


   go OnlyWriteData(ch1)
   go CalData(ch2, ch1)


   myPrinter(ch2)
}

咱們模擬 2 個通道,

  • 一個 只寫 不能讀
  • 一個 只讀 不能寫

實際效果

0
2
4
6
8
10
12
14
16
18

關閉通道

package main

import "fmt"

func main() {
   c := make(chan int)
   
   go func() {
      for i := 0; i < 10; i++ {
         // 循環向無緩衝的通道中寫入數據, 只有當上一個數據被讀走以後,下一個數據才能往通道中放
         c <- i
      }
      // 關閉通道
      close(c)
   }()
   for {
      // 讀取通道中的數據,若通道中無數據,則阻塞,若讀到 ok 爲false, 則通道關閉,退出循環
      if data, ok := <-c; ok {
         fmt.Println(data)
      } else {
         break
      }
   }
   fmt.Println("channel over")
}

再次強調一下關閉通道,demo 的模擬方式與上述的案例基本一致,感興趣的能夠本身運行看看效果

看到這裏,細心的小夥伴應該能夠總結出,判斷通道是否關閉的 2種 方式了吧?

  • 讀取通道的時候,判斷bool類型的變量是否爲false

例如上述代碼

if data, ok := <-c; ok {
    fmt.Println(data)
} else {
    break
}

判斷 ok 爲true,則正常讀取到數據, 若爲false ,則通道關閉

  • 經過 for range 的方式來遍歷通道,若退出循環,則是由於通道關閉

sync 包

Go 的 sync 包也是用做實現併發任務的同步

還記得嗎,在分享 文章GO的鎖和原子操做分享的時候,咱們就用到過 sync 包

用法大同消息,這裏列舉一下 sync 包涉及的數據結構和方法

  • sync.WaitGroup
  • sync.Once
  • sync.Map

sync.WaitGroup

他是一個結構體,傳遞的時候要傳遞指針 ,這裏須要注意

他是併發安全的,內部有維護一個計數器

涉及的方法:

  • (wg * WaitGroup) Add(delta int)

參數中 傳入的 delta ,表示 sync.WaitGroup 內部的計數器 + delta

  • (wg *WaitGroup) Done()

表示當前協程退出,計數器 -1

  • (wg *WaitGroup) Wait()

等待併發任務執行完畢,此時的計數器爲變成 0

sync.Once

他是併發安全的,內部有互斥鎖 和 一個布爾類型的數據

  • 互斥鎖 用於加鎖解鎖
  • 布爾類型的數據 用於記錄初始化是否完成

通常用於在高併發的場景下只執行一次,咱們一會兒就能想到的場景會有程序啓動時,加載配置文件的場景

針對相似的場景,Go 也給咱們提供瞭解決方法 ,即 sync.Once 裏面的 Do 方法

  • func (o *Once) Do(f func()) {}

Do 方法的參數 是一個函數,但是咱們要在該函數裏面傳遞參數咋整?

可使用Go 裏面的閉包來實現 , 閉包的具體實現方式,感興趣的能夠深刻了解一下

sync.Map

他是併發安全的,正是由於 Go 中的 map 是併發不安全的,所以有了 sync.Map

sync.Map 有以下幾個明顯的優點:

  • 併發安全
  • sync.Map 不須要使用 make 初始化,直接使用 myMap := sync.Map{} 便可使用 sync.Map 裏面的方法

sync.Map 涉及的方法

見名知意

  • Store

存入 key 和value

  • Load

取出 某個key 對應的 value

  • LoadOrStore

取出 而且 存入 2個操做

  • Delete

刪除key 和 對應的 value

  • Range

遍歷全部key 和 對應的 value

總結

  • 通道是什麼,通道的種類
  • 無緩衝,有緩衝,單向通道具體對應什麼
  • 對於通道的具體實踐
  • 分享了關於通道的異常狀況整理
  • 簡單分享了sync包的使用

歡迎點贊,關注,收藏

朋友們,你的支持和鼓勵,是我堅持分享,提升質量的動力

好了,本次就到這裏,下一次 服務註冊與發現之 ETCD

技術是開放的,咱們的心態,更應是開放的。擁抱變化,向陽而生,努力向前行。

我是小魔童哪吒,歡迎點贊關注收藏,下次見~

相關文章
相關標籤/搜索