眾所周知,Go lang的作用域相對嚴格,數據之間的通信往往要依靠參數的傳遞,但如果想在多個協程任務中間做數據通信,就需要通道(channel)的參與,我們可以把數據封裝成一個對象,然後把這個對象的指針傳入某個通道變數中,另外一個協程從這個通道中讀出變數的指針,並處理其指向的記憶體對象。 通道的聲明與 ...
眾所周知,Go lang的作用域相對嚴格,數據之間的通信往往要依靠參數的傳遞,但如果想在多個協程任務中間做數據通信,就需要通道(channel)的參與,我們可以把數據封裝成一個對象,然後把這個對象的指針傳入某個通道變數中,另外一個協程從這個通道中讀出變數的指針,並處理其指向的記憶體對象。
通道的聲明與創建
package main
import "fmt"
func main() {
var a chan int
if a == nil {
fmt.Println("通道是空的, 不能使用,需要先創建通道")
a = make(chan int)
fmt.Printf("數據類型是: %T", a)
}
}
這裡註意,通道聲明之後還需要進行創建。
也可以通過海象操作符聲明並創建:
package main
import "fmt"
func main() {
a := make(chan int)
fmt.Printf("數據類型是: %T", a)
}
程式返回:
數據類型是: chan int%
如此,一個類型為整形的通道就創建好了。
此外,通道是引用數據類型:
package main
import (
"fmt"
)
func main() {
ch1 := make(chan int)
fmt.Printf("%T,%p\n", ch1, ch1)
test1(ch1)
}
func test1(ch chan int) {
fmt.Printf("%T,%p\n", ch, ch)
}
程式返回:
chan int,0x1400010e060
chan int,0x1400010e060
可以看到,在test1函數內和main函數內通道的地址是一樣的,所以他們指向的都是同一個通道。
通道的使用
通道創建之後,即可以在協程之間充當橋梁:
package main
import "fmt"
func job(ch1 chan int) {
ch1 <- 1
}
func main() {
ch1 := make(chan int)
fmt.Println(ch1)
go job(ch1)
data := <-ch1 // 從ch1通道中讀取數據
fmt.Println("data-->", data)
fmt.Println("main。。over。。。。")
}
這裡我們聲明一個函數job,把通道作為參數傳遞進去,註意這裡參數類型除了聲明通道本身以外,還得聲明通道具體的數據類型。
隨後在main函數中,可以理解為主協程,創建通道ch1,執行開啟協程任務job,在job函數內,往通道內傳遞數字1
接著,主協程獲取通道內由job協程傳遞的數據:
0x1400006a060
data--> 1
main。。over。。。。
藉此,就完成了數據的傳遞。
這裡需要註意通道的調用語法:
data := <- a // 讀取通道
a <- data // 寫入通道
同步阻塞
這裡需要註意的是,通道無論是寫入還是讀取,都是同步阻塞機制。即當有協程對通道進行操作的時候,其他協程都處於“等待”狀態,說白了,就是在“排隊”,在之前的一篇:併發與並行,同步和非同步,Go lang1.18入門精煉教程,由白丁入鴻儒,Go lang併發編程之GoroutineEP13,我們要麼通過sync.WaitGroup來阻塞主協程,或者通過time.Sleep(time.Second)方法來阻塞,就是怕主協程提前執行完,早成子協程來不及執行。
而通道的出現,就間接幫我們實現了“阻塞”主協程的目的。
比如,多個協程任務操作一個變數:
package main
import (
"fmt"
)
func job1(number int, squareop chan int) {
sum := 20
sum += number
squareop <- sum
}
func job2(number int, cubeop chan int) {
sum := 10
sum += number
cubeop <- sum
}
func main() {
number := 0
ch1 := make(chan int)
ch2 := make(chan int)
go job1(number, ch1)
go job2(number, ch2)
num1, num2 := <-ch1, <-ch2
fmt.Println("Final output", num1+num2)
}
這裡job1和job2兩個協程任務同時非同步執行,操作number變數,累加後往通道中寫入,程式返回:
Final output 30
理論上,如果是併發執行,返回值應該是20或者10,但由於通道的存在,造成協程任務阻塞,變回了同步執行,所以返回了30。
同時,我們需要註意死鎖問題,如果一個協程任務在一個通道上發送數據,那麼其他的協程任務應該接收數據,如果這種情況不發生,那麼程式將在運行時出現死鎖。
換句話說,你發送了,就得有人接收,只發不接,或者只收不發,都會變成死鎖。
此外,協程任務可以通過close(ch)方法來關閉通道:
package main
import (
"fmt"
)
func job(ch1 chan int) {
// 發送方:3條數據
for i := 0; i < 3; i++ {
ch1 <- i //將i寫入通道中
}
close(ch1) //將ch1通道關閉了。
}
func main() {
ch1 := make(chan int)
go job(ch1)
/*
子goroutine,寫出數據3個
每寫一個,阻塞一次,主程式讀取一次,解除阻塞
主goroutine:迴圈讀
每次讀取一個,堵塞一次,子程式,寫出一個,解除阻塞
發送發,關閉通道的--->接收方,接收到的數據是該類型的零值,以及false
*/
//主程式中獲取通道的數據
for {
v, ok := <-ch1 //其他goroutine,顯示的調用close方法關閉通道。
if !ok {
fmt.Println("已經讀取了所有的數據,", ok)
break
}
fmt.Println("取出數據:", v, ok)
}
fmt.Println("main...over....")
}
這裡將0到2寫入chl通道,然後關閉通道。主函數里有一個死迴圈。類似while,它輪詢通道是否在發送數據後,使用變數ok進行判斷。如果ok是假的,則意味著通道關閉,因此迴圈結束,否則將會繼續進行無限輪詢。
select關鍵字
select 是 Go lang裡面的一個流程式控制制結構,和switch關鍵字差不多,但是select會隨機執行一個可運行的通道通信,如果沒有通道通信可運行,它將阻塞,直到有通道通信可運行:
package main
import (
"fmt"
"time"
)
func job(ch1 chan int) {
time.Sleep(2 * time.Second)
ch1 <- 200
}
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go job(ch1)
go job(ch2)
select {
case num1 := <-ch1:
fmt.Println("ch1中取數據。。", num1)
case num2, ok := <-ch2:
if ok {
fmt.Println("ch2中取數據。。", num2)
} else {
fmt.Println("ch2通道已經關閉。。")
}
}
}
這裡select會隨機選擇一個可運行的通道通信邏輯,可能是ch1通道,也有可能是ch2通道:
➜ mydemo git:(master) ✗ go run "/Users/liuyue/wodfan/work/mydemo/hello.go"
ch1中取數據。。 200
➜ mydemo git:(master) ✗ go run "/Users/liuyue/wodfan/work/mydemo/hello.go"
ch1中取數據。。 200
➜ mydemo git:(master) ✗ go run "/Users/liuyue/wodfan/work/mydemo/hello.go"
ch2中取數據。。 200
➜ mydemo git:(master) ✗
結語
綜上,Golang的通道其實就是將協程任務進行隔離,編寫併發邏輯時,關註通道即可,說白了,Golang的通道就是Python多進程通信中的管道,Golang雖然沒有顯性的多進程調用,但其協程調度底層就是多進程之間的通信,因為只有多進程才可能利用CPU的多核資源。