GO語言使用之channel(管道)
一、為什麼需要channel
1、需求:
現在要計算 1-200 的各個數的階乘,並且把各個數的階乘放入到map中。最後顯示出來。要求使用goroutine完成
1)、分析思路:
使用goroutine 來完成,效率高,但是會出現併發/並行安全問題.
這裡就提出了不同goroutine如何通訊的問題
2)、程式碼實現
使用goroutine來完成(看看使用gorotine併發完成會出現什麼問題? 然後我們會去解決)
在執行某個程式時,如何知道是否存在資源競爭問題。 方法很簡單,在編譯該程式時,增加一個引數 -race
即可
3)、示意圖
4)、程式碼實現:
package utils
import (
"sync"
"fmt"
"time"
)
// 需求:現在要計算 1-200 的各個數的階乘,並且把各個數的階乘放入到map中。
// 最後顯示出來。要求使用goroutine完成
// 思路
// 1. 編寫一個函式,來計算各個數的階乘,並放入到 map中.
// 2. 我們啟動的協程多個,統計的將結果放入到 map中
// 3. map 應該做出一個全域性的.
var (
myMap = make(map[int]int, 10)
)
// cacluFactorial 函式就是計算 n!, 讓將這個結果放入到 myMap
func cacluFactorial(n int ) {
res := 1
for i := 1; i <= n; i++ {
res *= i
}
//這裡我們將 res 放入到myMap
myMap[n] = res //concurrent map writes?
}
func FactorialDemo() {
// 我們這裡開啟多個協程完成這個任務[200個]
for i := 1; i <= 200; i++ {
go cacluFactorial(i)
}
//休眠10秒鐘【第二個問題 】
time.Sleep(time.Second * 10 )
//這裡我們輸出結果,變數這個結果
for i, v := range myMap {
fmt.Printf("map[%d]=%d\n", i, v)
}
}
2、不同goroutine之間如何通訊
- 全域性變數的互斥鎖
- 使用管道channel來解決
3、使用全域性變數加鎖同步改程序序
因為沒有對全域性變數 m 加鎖,因此會出現資源爭奪問題,程式碼會出現錯誤,提示concurrent map writes
解決方案:加入互斥鎖
我們的數的階乘很大,結果會越界,可以將求階乘改成 sum += uint64(i)
程式碼改進
package utils
import (
"sync"
"fmt"
"time"
)
// 需求:現在要計算 1-200 的各個數的階乘,並且把各個數的階乘放入到map中。
// 最後顯示出來。要求使用goroutine完成
// 思路
// 1. 編寫一個函式,來計算各個數的階乘,並放入到 map中.
// 2. 我們啟動的協程多個,統計的將結果放入到 map中
// 3. map 應該做出一個全域性的.
var (
myMap = make(map[int]int, 10)
/*同步鎖改進程式碼*/
//宣告一個全域性互斥鎖
lock sync.Mutex //sync包提供了基本的同步基元,如互斥鎖。Mutex是一個互斥鎖,
)
// cacluFactorial 函式就是計算 n!, 讓將這個結果放入到 myMap
func cacluFactorial(n int) {
res := 1
for i := 1; i <= n; i++ {
res *= i
}
/*同步鎖改進程式碼*/
lock.Lock()//加鎖
//這裡我們將 res 放入到myMap
myMap[n] = res //concurrent map writes?
lock.Unlock()//解鎖
}
func FactorialDemo() {
// 我們這裡開啟多個協程完成這個任務[200個]
for i := 1; i <= 200; i++ {
go cacluFactorial(i)
}
//休眠10秒鐘【第二個問題 】
time.Sleep(time.Second * 10)
/*同步鎖改進程式碼*/
lock.Lock()//加鎖
//這裡我們輸出結果,變數這個結果
for i, v := range myMap {
fmt.Printf("map[%d]=%d\n", i, v)
}
lock.Unlock()//解鎖
}
4、為什麼需要channel
前面使用全域性變數加鎖同步來解決goroutine的通訊,但不完美。
主執行緒在等待所有goroutine全部完成的時間很難確定,我們這裡設定10秒,僅僅是估算。
如果主執行緒休眠時間長了,會加長等待時間,如果等待時間短了,可能還有goroutine處於工作狀態,這時也會隨主執行緒的退出而銷燬。
通過全域性變數加鎖同步來實現通訊,也並不利用多個協程對全域性變數的讀寫操作。
上面種種分析都在呼喚一個新的通訊機制-channel
二、channel的基本介紹
1、channle本質就是一個數據結構-佇列
資料是先進先出【FIFO : first in first out】
執行緒安全,多goroutine訪問時,不需要加鎖,就是說channel本身就是執行緒安全的
channel有型別的,一個string的channel只能存放string型別資料。
示意圖:
2、定義/宣告channel
語法
var 變數名 chan 資料型別
舉例:
var intChan chan int (intChan用於存放int資料)
var mapChan chan map[int]string (mapChan用於存放map[int]string型別)
var perChan chan Person
var perChan2 chan *Person
…
說明
channel是引用型別
channel必須初始化才能寫入資料, 即make後才能使用
管道是有型別的,intChan 只能寫入 整數 int
三、快速入門案例
package utils
import (
"fmt"
)
//管道的初始化,寫入資料到管道,從管道讀取資料及基本的注意事項
func main() {
//演示一下管道的使用
//1. 建立一個可以存放3個int型別的管道
var intChan chan int
intChan = make(chan int, 3)
//2. 看看intChan是什麼
fmt.Printf("intChan 的值=%v intChan本身的地址=%p\n", intChan, &intChan)
//3. 向管道寫入資料
intChan<- 10
num := 211
intChan<- num
intChan<- 50
// intChan<- 98//注意點, 當我們給管寫入資料時,不能超過其容量
//4. 看看管道的長度和cap(容量)
fmt.Printf("channel len= %v cap=%v \n", len(intChan), cap(intChan)) // 3, 3
//5. 從管道中讀取資料
var num2 int
num2 = <-intChan
fmt.Println("num2=", num2)
fmt.Printf("channel len= %v cap=%v \n", len(intChan), cap(intChan)) // 2, 3
//6. 在沒有使用協程的情況下,如果我們的管道資料已經全部取出,再取就會報告 deadlock
num3 := <-intChan
num4 := <-intChan
num5 := <-intChan
fmt.Println("num3=", num3, "num4=", num4, "num5=", num5)
}
測試結果:
channel len= 3 cap=3
num2= 10
channel len= 2 cap=3
fatal error: all goroutines are asleep - deadlock!
總結:channel使用的注意事項
channel中只能存放指定的資料型別
channle的資料放滿後,就不能再放入了
如果從channel取出資料後,可以繼續放入
在沒有使用協程的情況下,如果channel資料取完了,再取,就會報dead lock
四、channel的遍歷和關閉
1 、channel的關閉
使用內建函式close可以關閉channel, 當channel關閉後,就不能再向channel寫資料了,但是仍然可以從該channel讀取資料
2、channel的遍歷
channel支援for–range的方式進行遍歷,請注意兩個細節
在遍歷時,如果channel沒有關閉,則回出現deadlock的錯誤
在遍歷時,如果channel已經關閉,則會正常遍歷資料,遍歷完後,就會退出遍歷。
3、案列演示
package utils
import (
"fmt"
)
func TranslateDemo() {
intChan := make(chan int, 3)
intChan<- 100
intChan<- 200
close(intChan) // close
//這是不能夠再寫入數到channel
//intChan<- 300
fmt.Println("okook~")
//當管道關閉後,讀取資料是可以的
n1 := <-intChan
fmt.Println("n1=", n1)
//遍歷管道
intChan2 := make(chan int, 100)
for i := 0; i < 100; i++ {
intChan2<- i * 2 //放入100個數據到管道
}
//遍歷管道不能使用普通的 for 迴圈
// for i := 0; i < len(intChan2); i++ {
// }
//在遍歷時,如果channel沒有關閉,則會出現deadlock的錯誤
//在遍歷時,如果channel已經關閉,則會正常遍歷資料,遍歷完後,就會退出遍歷
close(intChan2)
for v := range intChan2 {
fmt.Println("v=", v)
}
}
五、 goroutine和channel結合案例
package utils
import (
"fmt"
)
/*goroutine和channel結合
請完成goroutine和channel協同工作的案例,具體要求:
開啟一個writeData協程,向管道intChan中寫入50個整數.
開啟一個readData協程,從管道intChan中讀取writeData寫入的資料。
注意: writeData和readDate操作的是同一個管道
主執行緒需要等待writeData和readDate協程都完成工作才能退出【管道】
*/
func writeData(intChan chan int) {
for i := 1; i <= 50; i++ {
//放入資料
intChan<- i
fmt.Println("writeData ", i)
//time.Sleep(time.Second)
}
close(intChan) //關閉
}
//read data
func readData(intChan chan int, exitChan chan bool) {
for {
v, ok := <-intChan
if !ok {
break
}
//time.Sleep(time.Second)
fmt.Printf("readData 讀到資料=%v\n", v)
}
//readData 讀取完資料後,即任務完成
exitChan<- true
close(exitChan)
}
func Test() {
//建立兩個管道
intChan := make(chan int, 50)
exitChan := make(chan bool, 1)
go writeData(intChan)
go readData(intChan, exitChan)
//time.Sleep(time.Second * 10)
for {
_, ok := <-exitChan
if !ok {
break
}
}
}
六、 channel使用細節和注意事項
1、channel可以宣告為只讀,或者只寫性質 【案例演示】
2、channel只讀和只寫的最佳實踐案例
3、使用select可以解決從管道取資料的阻塞問題【案例演示】
package utils
// 使用select可以解決從管道取資料的阻塞問題
import (
"fmt"
"time"
)
func SelectDemo() {
//使用select可以解決從管道取資料的阻塞問題
//1.定義一個管道 10個數據int
intChan := make(chan int, 10)
for i := 0; i < 10; i++ {
intChan<- i
}
//2.定義一個管道 5個數據string
stringChan := make(chan string, 5)
for i := 0; i < 5; i++ {
stringChan <- "hello" + fmt.Sprintf("%d", i)
}
//傳統的方法在遍歷管道時,如果不關閉會阻塞而導致 deadlock
//問題,在實際開發中,可能我們不好確定什麼關閉該管道.
//可以使用select 方式可以解決
//label:
for {
select {
//注意: 這裡,如果intChan一直沒有關閉,不會一直阻塞而deadlock
//,會自動到下一個case匹配
case v := <-intChan :
fmt.Printf("從intChan讀取的資料%d\n", v)
time.Sleep(time.Second)
case v := <-stringChan :
fmt.Printf("從stringChan讀取的資料%s\n", v)
time.Sleep(time.Second)
default :
fmt.Printf("都取不到了,不玩了, 程式設計師可以加入邏輯\n")
time.Sleep(time.Second)
return
//break label
}
}
}
4、goroutine中使用recover,解決協程中出現panic,導致程式崩潰問題.【案例演示】
說明: 如果我們起了一個協程,但是這個協程出現了panic, 如果我們沒有捕獲這個panic,就會造成整個程式崩潰,這時我們可以在goroutine中使用recover來捕獲panic, 進行處理,這樣即使這個協程發生的問題,但是主執行緒仍然不受影響,可以繼續執行。
package utils
import (
"fmt"
"time"
)
//函式
func sayHello() {
for i := 0; i < 10; i++ {
time.Sleep(time.Second)
fmt.Println("hello,world")
}
}
//函式
func testRecover() {
//這裡我們可以使用defer + recover
defer func() {
//捕獲test丟擲的panic
if err := recover(); err != nil {
fmt.Println("test() 發生錯誤", err)
}
}()
//定義了一個map
var myMap map[int]string
myMap[0] = "golang" //error
}
func RecoverDemo() {
go sayHello()
go testRecover()
for i := 0; i < 10; i++ {
fmt.Println("main() ok=", i)
time.Sleep(time.Second)
}
}