[TOC]shell
咱們一塊兒回顧一下上次分享的內容:安全
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函數 獲取通道的容量
通道默認是既能夠讀有能夠寫的,可是單向通道就是要麼只能讀,要麼只能寫
是一個只能發送的通道,能夠發送可是不能接收
是一個只能接收的通道,能夠接收可是不能發送
在 Go 裏面,channel是一種類型,默認就是一種引用類型
簡單解釋一下什麼是引用:
在咱們寫C++的時候,用到引用會比較多
引用,顧名思義是某一個變量或對象的別名,對引用的操做與對其所綁定的變量或對象的操做徹底等價
在C++裏面是這樣用的:
類型 &引用名=目標變量名;
聲明一個通道
var 變量名 chan 元素類型 var ch1 chan string // 聲明一個傳遞字符串數據的通道 var ch2 chan []int // 聲明一個傳遞int切片數據的通道 var ch3 chan bool // 聲明一個傳遞布爾型數據的通道 var ch4 chan interface{} // 聲明一個傳遞接口類型數據的通道
看,聲明一個通道就是這麼簡單
對於通道來講,關聲明瞭還不能使用,聲明的通道默認是其對應類型的零值,例如
咱們還須要對通道進行初始化才能夠正常使用通道哦
通常是使用 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 通道,均可以怎麼玩
通道的操做有以下三種操做:
對於發送和接收通道里面的數據,寫法就比較形象,使用 <- 來指向是從通道里面讀取數據,仍是從通道中發送數據
向通道發送數據
// 建立一個通道 ch := make(chan int) // 發送數據給通道 ch <- 1
咱們看到箭頭的方向是,1 指向了 ch 通道,因此不難理解,這是將1 這個數據,放入通道中
從通道中接收數據
num := <-ch
不難看出,上述代碼是 ch 指向了一個須要初始化的變量,也就是說,從 ch 中讀出一個數據,賦值給 num
咱們從通道中讀出數據,也能夠不進行賦值,直接忽略也是能夠的,如:
<-ch
關閉通道
Go中提供了 close 函數來關閉通道
close(ch)
對於關閉通道很是須要注意,用很差直接致使程序崩潰
關閉後的通道有如下 4 個特色:
咱們來整理一下對於通道會存在的異常:
channel 狀態 | 未初始化的通道(nil) | 通道非空 | 通道是空的 | 通道滿了 | 通道未滿 |
---|---|---|---|---|---|
接收數據 | 阻塞 |
接收數據 | 阻塞 |
接收數據 | 接收數據 |
發送數據 | 阻塞 |
發送數據 | 發送數據 | 阻塞 |
發送數據 |
關閉 | panic | 關閉通道成功 待數據讀取完畢後 返回零值 |
關閉通道成功 直接返回零值 |
關閉通道成功 待數據讀取完畢後 返回零值 |
關閉通道成功 待數據讀取完畢後 返回零值 |
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種 方式了吧?
例如上述代碼
if data, ok := <-c; ok { fmt.Println(data) } else { break }
判斷 ok 爲true,則正常讀取到數據, 若爲false ,則通道關閉
sync 包
Go 的 sync 包也是用做實現併發任務的同步
還記得嗎,在分享 文章GO的鎖和原子操做分享的時候,咱們就用到過 sync 包
用法大同消息,這裏列舉一下 sync 包涉及的數據結構和方法
sync.WaitGroup
他是一個結構體,傳遞的時候要傳遞指針 ,這裏須要注意
他是併發安全的,內部有維護一個計數器
涉及的方法:
參數中 傳入的 delta ,表示 sync.WaitGroup 內部的計數器 + delta
表示當前協程退出,計數器 -1
等待併發任務執行完畢,此時的計數器爲變成 0
sync.Once
他是併發安全的,內部有互斥鎖 和 一個布爾類型的數據
通常用於在高併發的場景下只執行一次,咱們一會兒就能想到的場景會有程序啓動時,加載配置文件的場景
針對相似的場景,Go 也給咱們提供瞭解決方法 ,即 sync.Once 裏面的 Do 方法
Do 方法的參數 是一個函數,但是咱們要在該函數裏面傳遞參數咋整?
可使用Go 裏面的閉包來實現 , 閉包的具體實現方式,感興趣的能夠深刻了解一下
sync.Map
他是併發安全的,正是由於 Go 中的 map 是併發不安全的,所以有了 sync.Map
sync.Map 有以下幾個明顯的優點:
myMap := sync.Map{}
便可使用 sync.Map 裏面的方法sync.Map 涉及的方法
見名知意
存入 key 和value
取出 某個key 對應的 value
取出 而且 存入 2個操做
刪除key 和 對應的 value
遍歷全部key 和 對應的 value
朋友們,你的支持和鼓勵,是我堅持分享,提升質量的動力
好了,本次就到這裏,下一次 服務註冊與發現之 ETCD
技術是開放的,咱們的心態,更應是開放的。擁抱變化,向陽而生,努力向前行。
我是小魔童哪吒,歡迎點贊關注收藏,下次見~