GO語言中通道和sync包的使用教程分享
GO通道和 sync 包的分享
我們一起回顧一下上次分享的內(nèi)容:
- GO協(xié)程同步若不做限制的話,會產(chǎn)生數(shù)據(jù)競態(tài)的問題
- 我們用鎖的方式來解決如上問題,根據(jù)使用場景選擇使用互斥鎖 和 讀寫鎖
- 比使用鎖更好的方式是原子操作,但是使用go的
sync/atomic
需要小心使用,因為涉及內(nèi)存
要是對GO的鎖和原子操作還感興趣的話,歡迎查看文章GO的鎖和原子操作分享
上次我們分享到鎖和原子操作,都可以保證共享數(shù)據(jù)的讀寫
可是,他們還是會影響性能,不過,Go 為開發(fā)這提供了 通道 這個神器
今天我們來分享一下Go中推薦使用的其他同步方法,通道和 sync 包
通道是什么
是一種特殊的類型,是連接并發(fā)goroutine
的管道
channel 通道是可以讓一個 goroutine 協(xié)程發(fā)送特定值到另一個 goroutine 協(xié)程的通信機制。
通道像一個傳送帶或者隊列,總是遵循先入先出(First In First Out)的規(guī)則,保證收發(fā)數(shù)據(jù)的順序,這一點和管道是一樣的
一個協(xié)程從通道的一頭放入數(shù)據(jù),另一個協(xié)程從通道的另一頭讀出數(shù)據(jù)
每一個通道都是一個具體類型的導(dǎo)管,聲明 channel 的時候需要為其指定元素類型。
通道能做什么
控制協(xié)程的同步,讓程序有序運行
GO 中提倡 不要通過共享內(nèi)存來通信,而通過通信來共享內(nèi)存
goroutine協(xié)程 是 Go 程序并發(fā)的執(zhí)行體,channel 通道就是它們之間的連接,他們之間的橋梁,他們的交通樞紐
通道有哪幾種
大致可分為如下三種:
- 無緩沖通道
- 有緩沖的通道
- 單向通道
無緩沖通道
無緩沖的通道又稱為阻塞的通道
無緩沖通道上的發(fā)送操作會阻塞,直到另一個goroutine在該通道上執(zhí)行接收操作,這時值才能發(fā)送成功
兩個 goroutine 協(xié)程將繼續(xù)執(zhí)行
我們反過來看,如果接收操作先執(zhí)行,接收方的goroutine將阻塞,直到另一個 goroutine 協(xié)程在該通道上發(fā)送一個數(shù)據(jù)
因此,無緩沖通道也被稱為同步通道,因為我們可以使用無緩沖通道進行通信,利用發(fā)送和接收的 goroutine 協(xié)程同步化
有緩沖的通道
還是上述提到的,有緩沖通道,就是在初始化 / 創(chuàng)建通道 的 make 函數(shù)的第 2 個參數(shù)填上我們所期望的緩沖區(qū)大小 , 例如:
ch1 := make(chan int , 4)
此時,該通道的容量為4,發(fā)送方可以一直向通道中發(fā)送數(shù)據(jù),直到通道滿,且通道數(shù)據(jù)未被讀走時,發(fā)送方就會阻塞
只要通道的容量大于零,那么該通道就是有緩沖的通道
通道的容量表示通道中能存放元素的數(shù)量
我們可以使用內(nèi)置的 len函數(shù) 獲取通道內(nèi)元素的數(shù)量,使用 cap函數(shù) 獲取通道的容量
單向通道
通道默認是既可以讀有可以寫的,但是單向通道就是要么只能讀,要么只能寫
1.chan <- int
是一個只能發(fā)送的通道,可以發(fā)送但是不能接收
2.<- chan int
是一個只能接收的通道,可以接收但是不能發(fā)送
如何創(chuàng)建和聲明一個通道
聲明通道
在 Go 里面,channel是一種類型,默認就是一種引用類型
簡單解釋一下什么是引用:
- 在我們寫C++的時候,用到引用會比較多
- 引用,顧名思義是某一個變量或?qū)ο蟮膭e名,對引用的操作與對其所綁定的變量或?qū)ο蟮牟僮魍耆葍r
- 在C++里面是這樣用的:
- 類型 &引用名=目標變量名;
聲明一個通道
var 變量名 chan 元素類型 var ch1 chan string // 聲明一個傳遞字符串?dāng)?shù)據(jù)的通道 var ch2 chan []int // 聲明一個傳遞int切片數(shù)據(jù)的通道 var ch3 chan bool // 聲明一個傳遞布爾型數(shù)據(jù)的通道 var ch4 chan interface{} // 聲明一個傳遞接口類型數(shù)據(jù)的通道
看,聲明一個通道就是這么簡單
對于通道來說,關(guān)聲明了還不能使用,聲明的通道默認是其對應(yīng)類型的零值,例如
- int 類型 零值 就是 0
- string 類型 零值就是個 空串
- bool 類型 零值就是 false
- 切片的 零值 就是 nil
我們還需要對通道進行初始化才可以正常使用通道哦
初始化通道
一般是使用 make 函數(shù)初始化之后才能使用通道,也可以直接使用make函數(shù) 創(chuàng)建通道
例如:
ch5 := make(chan string) ch6 := make(chan []int) ch7 := make(chan bool) ch8 := make(chan interface{})
make 函數(shù)的第二個參數(shù)是可以設(shè)置緩沖的大小的,我們來看看源碼的說明
// 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 函數(shù)的第二個參數(shù)不填,那么就默認是無緩沖的通道
現(xiàn)在我們來看看如何操作 channel 通道,都可以怎么玩
如何操作 channel
通道的操作有如下三種操作:
- 發(fā)送(send)
- 接收(receive)
- 關(guān)閉(close)
對于發(fā)送和接收通道里面的數(shù)據(jù),寫法就比較形象,使用 <- 來指向是從通道里面讀取數(shù)據(jù),還是從通道中發(fā)送數(shù)據(jù)
向通道發(fā)送數(shù)據(jù)
// 創(chuàng)建一個通道 ch := make(chan int) // 發(fā)送數(shù)據(jù)給通道 ch <- 1
我們看到箭頭的方向是,1 指向了 ch 通道,所以不難理解,這是將1 這個數(shù)據(jù),放入通道中
從通道中接收數(shù)據(jù)
num := <-ch
不難看出,上述代碼是 ch 指向了一個需要初始化的變量,也就是說,從 ch 中讀出一個數(shù)據(jù),賦值給 num
我們從通道中讀出數(shù)據(jù),也可以不進行賦值,直接忽略也是可以的,如:
<-ch
關(guān)閉通道
Go中提供了 close 函數(shù)來關(guān)閉通道
close(ch)
對于關(guān)閉通道非常需要注意,用不好直接導(dǎo)致程序崩潰
- 只有在通知接收方 goroutine 協(xié)程所有的數(shù)據(jù)都發(fā)送完畢的時候才需要關(guān)閉通道
- 通道是可以被垃圾回收機制回收的,它和關(guān)閉文件是不一樣的,在結(jié)束操作之后關(guān)閉文件是必須要做的,但關(guān)閉通道不是必須的
關(guān)閉后的通道有以下 4 個特點:
- 對一個關(guān)閉的通道再發(fā)送值就會導(dǎo)致 panic
- 對一個關(guān)閉的通道進行接收會一直獲取值直到通道為空
- 對一個關(guān)閉的并且沒有值的通道執(zhí)行接收操作會得到對應(yīng)類型的零值
- 關(guān)閉一個已經(jīng)關(guān)閉的通道會導(dǎo)致 panic
通道異常情況梳理
我們來整理一下對于通道會存在的異常:
channel 狀態(tài) | 未初始化的通道(nil) | 通道非空 | 通道是空的 | 通道滿了 | 通道未滿 |
---|---|---|---|---|---|
接收數(shù)據(jù) | 阻塞 | 接收數(shù)據(jù) | 阻塞 | 接收數(shù)據(jù) | 接收數(shù)據(jù) |
發(fā)送數(shù)據(jù) | 阻塞 | 發(fā)送數(shù)據(jù) | 發(fā)送數(shù)據(jù) | 阻塞 | 發(fā)送數(shù)據(jù) |
關(guān)閉 | panic | 關(guān)閉通道成功 待數(shù)據(jù)讀取完畢后 返回零值 | 關(guān)閉通道成功 直接返回零值 | 關(guān)閉通道成功 待數(shù)據(jù)讀取完畢后 返回零值 | 關(guān)閉通道成功 待數(shù)據(jù)讀取完畢后 返回零值 |
每一種通道的DEMO實戰(zhàn)
無緩沖通道
func main() { // 創(chuàng)建一個無緩沖的,數(shù)據(jù)類型 為 int 類型的通道 ch := make(chan int) // 向通道中寫入 數(shù)字 1 ch <- 1 fmt.Println("send successfully ... ") }
執(zhí)行上述代碼我們可以查看到效果
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
F:/my_channel/main.go:9 +0x45
exit status 2
出現(xiàn)上述報錯 deadlock 錯誤的原因,細心的小伙伴應(yīng)該能夠知道為什么,我上述有提到
我們使用 ch := make(chan int)
創(chuàng)建的是無緩沖的通道
無緩沖的通道只有在有接收方接收值的時候才能發(fā)送數(shù)據(jù)成功
我們可以想一下我們生活中的案例一樣:
你在某東上買了一個稍微貴重一點的物品,某東快遞人員給你寄快遞的時候,打電話給你,必須要送到你的手上,不然不敢簽收,這個時候,你不方便,或者你不簽收,那么這個快遞就是算作沒有寄送成功
因此,上述問題原因是,創(chuàng)建了一個無緩沖通道,發(fā)送方一直在阻塞,通道中一直未有協(xié)程讀取數(shù)據(jù),導(dǎo)致死鎖
我們的解決辦法就是創(chuàng)建另外一個協(xié)程,將數(shù)據(jù)從通道中讀出來即可
package main import "fmt" func recvData(c chan int) { ret := <-c fmt.Println("recvData successfully ... data = ", ret) } func main() { // 創(chuàng)建一個無緩沖的,數(shù)據(jù)類型 為 int 類型的通道 ch := make(chan int) go recvData(ch) // 向通道中寫入 數(shù)字 1 ch <- 1 fmt.Println("send successfully ... ") }
這里需要注意,如果 go recvData(ch)
放在了 ch <- 1
之后,那么結(jié)果還是一樣的死鎖,原因還是因為 ch <- 1
會一直阻塞,根本不會執(zhí)行到 他之后的語句
實際效果
recvData successfully ... data = 1
send successfully ...
有緩沖通道
func main() { // 創(chuàng)建一個無緩沖的,數(shù)據(jù)類型 為 int 類型的通道 ch := make(chan int , 1) // 向通道中寫入 數(shù)字 1 ch <- 1 fmt.Println("send successfully ... ") }
還是同樣的案例,同樣的代碼,我們只是把無緩沖通道,換成了有緩沖的通道, 我們?nèi)匀徊粚iT開協(xié)程讀取通道的數(shù)據(jù)
實際效果 , 發(fā)送成功
send successfully ...
因為此時通道中的緩沖是1,第一次向通道中發(fā)送數(shù)據(jù),不會阻塞,
可是如果,在通道中數(shù)據(jù)還未讀取出去之前,又向通道中寫入數(shù)據(jù),則此處會阻塞,
若一直沒有協(xié)程從通道中讀取數(shù)據(jù),則結(jié)果與上述一樣,會死鎖
單向通道
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通道 數(shù)據(jù)讀取完畢,則阻塞,若in 通道關(guān)閉,則退出循環(huán) for i := range in { out <- i + i } close(out) } func myPrinter(in <-chan int) { // 遍歷 讀取in 通道,若 in通道 數(shù)據(jù)讀取完畢,則阻塞,若in 通道關(guān)閉,則退出循環(huán) for i := range in { fmt.Println(i) } } func main() { // 創(chuàng)建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
關(guān)閉通道
package main import "fmt" func main() { c := make(chan int) go func() { for i := 0; i < 10; i++ { // 循環(huán)向無緩沖的通道中寫入數(shù)據(jù), 只有當(dāng)上一個數(shù)據(jù)被讀走之后,下一個數(shù)據(jù)才能往通道中放 c <- i } // 關(guān)閉通道 close(c) }() for { // 讀取通道中的數(shù)據(jù),若通道中無數(shù)據(jù),則阻塞,若讀到 ok 為false, 則通道關(guān)閉,退出循環(huán) if data, ok := <-c; ok { fmt.Println(data) } else { break } } fmt.Println("channel over") }
再次強調(diào)一下關(guān)閉通道,demo 的模擬方式與上述的案例基本一致,感興趣的可以自己運行看看效果
看到這里,細心的小伙伴應(yīng)該可以總結(jié)出,判斷通道是否關(guān)閉的 2種 方式了吧?
讀取通道的時候,判斷bool類型的變量是否為false
例如上述代碼
if data, ok := <-c; ok { fmt.Println(data) } else { break }
判斷 ok 為true,則正常讀取到數(shù)據(jù), 若為false ,則通道關(guān)閉
通過 for range 的方式來遍歷通道,若退出循環(huán),則是因為通道關(guān)閉
sync 包
Go 的 sync 包也是用作實現(xiàn)并發(fā)任務(wù)的同步
還記得嗎,在分享 文章GO的鎖和原子操作分享的時候,我們就用到過 sync 包
用法大同消息,這里列舉一下 sync 包涉及的數(shù)據(jù)結(jié)構(gòu)和方法
- sync.WaitGroup
- sync.Once
- sync.Map
sync.WaitGroup
他是一個結(jié)構(gòu)體,傳遞的時候要傳遞指針 ,這里需要注意
他是并發(fā)安全的,內(nèi)部有維護一個計數(shù)器
涉及的方法:
(wg * WaitGroup) Add(delta int)
參數(shù)中 傳入的 delta ,表示 sync.WaitGroup 內(nèi)部的計數(shù)器 + delta
(wg *WaitGroup) Done()
表示當(dāng)前協(xié)程退出,計數(shù)器 -1
(wg *WaitGroup) Wait()
等待并發(fā)任務(wù)執(zhí)行完畢,此時的計數(shù)器為變成 0
sync.Once
他是并發(fā)安全的,內(nèi)部有互斥鎖 和 一個布爾類型的數(shù)據(jù)
- 互斥鎖 用于加鎖解鎖
- 布爾類型的數(shù)據(jù) 用于記錄初始化是否完成
一般用于在高并發(fā)的場景下只執(zhí)行一次,我們一下子就能想到的場景會有程序啟動時,加載配置文件的場景
針對類似的場景,Go 也給我們提供了解決方法 ,即 sync.Once 里面的 Do 方法
func (o *Once) Do(f func()) {}
Do 方法的參數(shù) 是一個函數(shù),可是我們要在該函數(shù)里面?zhèn)鬟f參數(shù)咋整?
可以使用Go 里面的閉包來實現(xiàn) , 閉包的具體實現(xiàn)方式,感興趣的可以深入了解一下
sync.Map
他是并發(fā)安全的,正是因為 Go 中的 map 是并發(fā)不安全的,因此有了 sync.Map
sync.Map 有如下幾個明顯的優(yōu)勢:
- 并發(fā)安全
- sync.Map 不需要使用 make 初始化,直接使用
myMap := sync.Map{}
即可使用 sync.Map 里面的方法
sync.Map 涉及的方法
見名知意
Store
存入 key 和value
Load
取出 某個key 對應(yīng)的 value
LoadOrStore
取出 并且 存入 2個操作
Delete
刪除key 和 對應(yīng)的 value
Range
遍歷所有key 和 對應(yīng)的 value
總結(jié)
- 通道是什么,通道的種類
- 無緩沖,有緩沖,單向通道具體對應(yīng)什么
- 對于通道的具體實踐
- 分享了關(guān)于通道的異常情況整理
- 簡單分享了sync包的使用
以上就是GO語言中通道和sync包的使用教程分享的詳細內(nèi)容,更多關(guān)于GO通道 sync包的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
從錯誤中學(xué)習(xí)改正Go語言六個壞習(xí)慣提高編程技巧
這篇文章主要為大家介紹了從錯誤中學(xué)習(xí)改正Go語言五個壞習(xí)慣提高編程技巧示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-05-05解決golang 反射interface{}做零值判斷的一個重大坑
這篇文章主要介紹了解決golang 反射interface{}做零值判斷的一個重大坑,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-04-04GoLang strings.Builder底層實現(xiàn)方法詳解
自從學(xué)習(xí)go一個月以來,我多少使用了一下strings.Builder,略有心得。你也許知道它,特別是你了解bytes.Buffer的話。所以我在此分享一下我的心得,并希望能對你有所幫助2022-10-10Go實現(xiàn)數(shù)據(jù)脫敏的方案設(shè)計
在一些常見的業(yè)務(wù)場景中可能涉及到用戶的手機號,銀行卡號等敏感數(shù)據(jù),對于這部分的數(shù)據(jù)經(jīng)常需要進行數(shù)據(jù)脫敏處理,就是將此部分數(shù)據(jù)隱私化,防止數(shù)據(jù)泄露,所以本文給大家介紹了Go實現(xiàn)數(shù)據(jù)脫敏的方案設(shè)計,需要的朋友可以參考下2024-05-05golang中time包之時間間隔格式化和秒、毫秒、納秒等時間戳格式輸出的方法實例
時間和日期是我們編程中經(jīng)常會用到的,下面這篇文章主要給大家介紹了關(guān)于golang中time包之時間間隔格式化和秒、毫秒、納秒等時間戳格式輸出的方法實例,需要的朋友可以參考下2022-08-08