channel一個型別管道,通過它可以在goroutine之間傳送和接收訊息。它是Golang在語言層面提供的goroutine間的通訊方式。
眾所周知,Go依賴於稱為CSP(Communicating Sequential Processes)的併發模型,通過Channel實現這種同步模式。Go併發的核心哲學是不要通過共享記憶體進行通訊; 相反,通過溝通分享記憶。
下面以簡單的示例來演示Go如何通過channel來實現通訊。
package main
import (
"fmt"
"time"
)
func goRoutineA(a <-chan int) {
val := <-a
fmt.Println("goRoutineA received the data", val)
}
func goRoutineB(b chan int) {
val := <-b
fmt.Println("goRoutineB received the data", val)
}
func main() {
ch := make(chan int, 3)
go goRoutineA(ch)
go goRoutineB(ch)
ch <- 3
time.Sleep(time.Second * 1)
}
結果為:
goRoutineA received the data 3
上面只是個簡單的例子,只輸出goRoutineA ,沒有執行goRoutineB,說明channel僅允許被一個goroutine讀寫。
接下來我們通過原始碼分析程式執行過程,在講之前,如果不瞭解go 併發和排程相關知識。請閱讀這篇文章
https://github.com/guyan0319/...
說道channel這裡不得不提通道的結構hchan。
hchan
原始碼在src/runtime/chan.go
type hchan struct {
qcount uint // total data in the queue
dataqsiz uint // size of the circular queue
buf unsafe.Pointer // points to an array of dataqsiz elements
elemsize uint16
closed uint32
elemtype *_type // element type
sendx uint // send index
recvx uint // receive index
recvq waitq // list of recv waiters
sendq waitq // list of send waiters
// lock protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
lock mutex
}
type waitq struct {
first *sudog
last *sudog
}
說明:
qcount uint // 當前佇列中剩餘元素個數
dataqsiz uint // 環形佇列長度,即緩衝區的大小,即make(chan T,N),N.
buf unsafe.Pointer // 環形佇列指標
elemsize uint16 // 每個元素的大小
closed uint32 // 表示當前通道是否處於關閉狀態。建立通道後,該欄位設定為0,即通道開啟; 通過呼叫close將其設定為1,通道關閉。
elemtype *_type // 元素型別,用於資料傳遞過程中的賦值;
sendx uint和recvx uint是環形緩衝區的狀態欄位,它指示緩衝區的當前索引 - 支援陣列,它可以從中傳送資料和接收資料。
recvq waitq // 等待讀訊息的goroutine佇列
sendq waitq // 等待寫訊息的goroutine佇列
lock mutex // 互斥鎖,為每個讀寫操作鎖定通道,因為傳送和接收必須是互斥操作。
這裡sudog代表goroutine。
建立channel 有兩種,一種是帶緩衝的channel,一種是不帶緩衝的channel
// 帶緩衝
ch := make(chan Task, 3)
// 不帶緩衝
ch := make(chan int)
這裡我們先討論帶緩衝
ch := make(chan int, 3)
建立通道後的緩衝通道結構
hchan struct {
qcount uint : 0
dataqsiz uint : 3
buf unsafe.Pointer : 0xc00007e0e0
elemsize uint16 : 8
closed uint32 : 0
elemtype *runtime._type : &{
size:8
ptrdata:0
hash:4149441018
tflag:7
align:8
fieldalign:8
kind:130
alg:0x55cdf0
gcdata:0x4d61b4
str:1055
ptrToThis:45152
}
sendx uint : 0
recvx uint : 0
recvq runtime.waitq :
{first:<nil> last:<nil>}
sendq runtime.waitq :
{first:<nil> last:<nil>}
lock runtime.mutex :
{key:0}
}
原始碼
func makechan(t *chantype, size int) *hchan {
elem := t.elem
...
}
如果我們建立一個帶buffer的channel,底層的資料模型如下圖:
向channel寫入資料
ch <- 3
底層hchan資料流程如圖
傳送操作概要
1、鎖定整個通道結構。
2、確定寫入。嘗試recvq
從等待佇列中等待goroutine,然後將元素直接寫入goroutine。
3、如果recvq為Empty,則確定緩衝區是否可用。如果可用,從當前goroutine複製資料到緩衝區。
4、如果緩衝區已滿,則要寫入的元素將儲存在當前正在執行的goroutine的結構中,並且當前goroutine將在sendq中排隊並從執行時掛起。
5、寫入完成釋放鎖。
這裡我們要注意幾個屬性buf、sendx、lock的變化。
流程圖
從channel讀取操作
幾乎和寫入操作相同
程式碼
func goRoutineA(a <-chan int) {
val := <-a
fmt.Println("goRoutineA received the data", val)
}
底層hchan資料流程如圖
這裡我們要注意幾個屬性buf、sendx、recvx、lock的變化。
讀取操作概要
1、先獲取channel全域性鎖
2、嘗試sendq從等待佇列中獲取等待的goroutine,
3、 如有等待的goroutine,沒有緩衝區,取出goroutine並讀取資料,然後喚醒這個goroutine,結束讀取釋放鎖。
4、如有等待的goroutine,且有緩衝區(此時緩衝區已滿),從緩衝區隊首取出資料,再從sendq取出一個goroutine,將goroutine中的資料存入buf隊尾,結束讀取釋放鎖。
5、如沒有等待的goroutine,且緩衝區有資料,直接讀取緩衝區資料,結束讀取釋放鎖。
6、如沒有等待的goroutine,且沒有緩衝區或緩衝區為空,將當前的goroutine加入sendq排隊,進入睡眠,等待被寫goroutine喚醒。結束讀取釋放鎖。
流程圖
recvq和sendq 結構
recvq和sendq基本上是連結串列,看起來基本如下
select
select就是用來監聽和channel有關的IO操作,當 IO 操作發生時,觸發相應的動作。
一個簡單的示例如下
package main
import (
"fmt"
"time"
)
func goRoutineD(ch chan int, i int) {
time.Sleep(time.Second * 3)
ch <- i
}
func goRoutineE(chs chan string, i string) {
time.Sleep(time.Second * 3)
chs <- i
}
func main() {
ch := make(chan int, 5)
chs := make(chan string, 5)
go goRoutineD(ch, 5)
go goRoutineE(chs, "ok")
select {
case msg := <-ch:
fmt.Println(" received the data ", msg)
case msgs := <-chs:
fmt.Println(" received the data ", msgs)
default:
fmt.Println("no data received ")
time.Sleep(time.Second * 1)
}
}
執行程式,因為當前時間沒有到3s,所以select 選擇defult
no data received
修改程式,我們註釋掉default,並多執行幾次結果為
received the data 5
received the data ok
received the data ok
received the data ok
select語句會阻塞,直到監測到一個可以執行的IO操作為止,而這裡goRoutineD和goRoutineE睡眠時間是相同的,都是3s,從輸出可看出,從channel中讀出資料的順序是隨機的。
再修改程式碼,goRoutineD睡眠時間改成4s
func goRoutineD(ch chan int, i int) {
time.Sleep(time.Second * 4)
ch <- i
}
此時會先執行goRoutineE,select 選擇case msgs := <-chs。
range
可以持續從channel讀取資料,一直到channel被關閉,當channel中沒有資料時會阻塞當前goroutine,與讀channel時阻塞處理機制一樣。
package main
import (
"fmt"
"time"
)
func goRoutineD(ch chan int, i int) {
for i := 1; i <= 5; i++{
time.Sleep(time.Second * 1)
ch <- i
}
}
func chanRange(chanName chan int) {
for e := range chanName {
fmt.Printf("Get element from chan: %d\n", e)
if len(chanName) <= 0 { // 如果現有資料量為0,跳出迴圈
break
}
}
}
func main() {
ch := make(chan int, 5)
go goRoutineD(ch, 5)
chanRange(ch)
}
結果:
Get element from chan: 1
Get element from chan: 2
Get element from chan: 3
Get element from chan: 4
Get element from chan: 5
死鎖(deadlock)
指兩個或兩個以上的協程的執行過程中,由於競爭資源或由於彼此通訊而造成的一種阻塞的現象。
在非緩衝通道若發生只流入不流出,或只流出不流入,就會發生死鎖。
下面是一些死鎖的例子
1、
package main
func main() {
ch := make(chan int)
ch <- 3
}
上面情況,向非緩衝通道寫資料會發生阻塞,導致死鎖。解決辦法建立緩衝區 ch := make(chan int,3)
2、
package main
import (
"fmt"
)
func main() {
ch := make(chan int)
fmt.Println(<-ch)
}
向非緩衝通道讀取資料會發生阻塞,導致死鎖。 解決辦法開啟緩衝區,先向channel寫入資料。
3、
package main
func main() {
ch := make(chan int, 3)
ch <- 3
ch <- 4
ch <- 5
ch <- 6
}
寫入資料超過緩衝區數量也會發生死鎖。解決辦法將寫入資料取走。
4、
package main
func main() {
ch := make(chan int, 3)
ch <- 1
close(ch)
ch <- 2
}
向關閉的channel寫入資料。解決辦法別向關閉的channel寫入資料。
死鎖的情況有很多這裡不再贅述。
參考:
https://codeburst.io/diving-d...
https://speakerdeck.com/kavya...
https://my.oschina.net/renhc/...