日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

GO語言中通道和sync包的使用教程分享_Golang

作者:阿兵云原生 ? 更新時間: 2023-06-16 編程語言

GO通道和 sync 包的分享

我們一起回顧一下上次分享的內容:

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

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

上次我們分享到鎖和原子操作,都可以保證共享數據的讀寫

可是,他們還是會影響性能,不過,Go 為開發這提供了 通道 這個神器

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

通道是什么

是一種特殊的類型,是連接并發goroutine的管道

channel 通道是可以讓一個 goroutine 協程發送特定值到另一個 goroutine 協程的通信機制

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

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

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

通道能做什么

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

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

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

通道有哪幾種

大致可分為如下三種:

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

無緩沖通道

無緩沖的通道又稱為阻塞的通道

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

兩個 goroutine 協程將繼續執行

我們反過來看,如果接收操作先執行,接收方的goroutine將阻塞,直到另一個 goroutine 協程在該通道上發送一個數據

因此,無緩沖通道也被稱為同步通道,因為我們可以使用無緩沖通道進行通信,利用發送和接收的 goroutine 協程同步化

有緩沖的通道

還是上述提到的,有緩沖通道,就是在初始化 / 創建通道 的 make 函數的第 2 個參數填上我們所期望的緩沖區大小 , 例如:

ch1 := make(chan int , 4)

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

只要通道的容量大于零,那么該通道就是有緩沖的通道

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

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

單向通道

通道默認是既可以讀有可以寫的,但是單向通道就是要么只能讀,要么只能寫

1.chan <- int

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

2.<- 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包的使用

原文鏈接:https://juejin.cn/post/6973108979777929230

  • 上一篇:沒有了
  • 下一篇:沒有了
欄目分類
最近更新