Golang 通道(channel )源码分析(一、定义与创建关闭)
注意当前go版本代码为1.23
Channel 在运行时的内部表示是 runtime.hchan
定义
Go 语言的 Channel 在运行时使用 runtime.hchan
结构体表示,源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| type hchan struct { qcount uint dataqsiz uint buf unsafe.Pointer elemsize uint16 closed uint32 timer *timer elemtype *_type sendx uint recvx uint recvq waitq sendq waitq
lock mutex }
type waitq struct { first *sudog last *sudog }
|
buf
指向底层循环数组,只有缓冲型的 channel 才有。
sendx
,recvx
均指向底层循环数组,表示当前可以发送和接收的元素位置索引值(相对于底层数组)。
sendq
,recvq
分别表示被阻塞的 goroutine,这些 goroutine 由于尝试读取 channel 或向 channel 发送数据而被阻塞。
waitq
是 sudog
的一个双向链表,而 sudog
实际上是对 goroutine 的一个封装:runtime.sudog
表示一个在等待列表中的 Goroutine,该结构中存储了两个分别指向前后 runtime.sudog
的指针以构成链表。
例如,创建一个容量为 6 的,元素为 int 型的 channel 数据结构如下(采用自Go 程序员面试笔试宝典) :
创建
源码位置:runtime.makechan
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| func makechan(t *chantype, size int) *hchan { elem := t.Elem
mem, overflow := math.MulUintptr(elem.Size_, uintptr(size)) if overflow || mem > maxAlloc-hchanSize || size < 0 { panic(plainError("makechan: 尺寸超出范围")) }
var c *hchan switch { case mem == 0: c = (*hchan)(mallocgc(hchanSize, nil, true)) c.buf = c.raceaddr() case !elem.Pointers(): c = (*hchan)(mallocgc(hchanSize+mem, nil, true)) c.buf = add(unsafe.Pointer(c), hchanSize) default: c = new(hchan) c.buf = mallocgc(mem, elem, true) }
c.elemsize = uint16(elem.Size_) c.elemtype = elem c.dataqsiz = uint(size) lockInit(&c.lock, lockRankHchan)
if debugChan { print("makechan: chan=", c, "; elemsize=", elem.Size_, "; dataqsiz=", size, "\n") } return c }
|
关闭
源码位置:runtime.closechan
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
|
func closechan(c *hchan) {
c.closed = 1
var glist gList
for { sg := c.recvq.dequeue() if sg == nil { break } if sg.elem != nil { typedmemclr(c.elemtype, sg.elem) sg.elem = nil } if sg.releasetime != 0 { sg.releasetime = cputicks() } gp := sg.g gp.param = unsafe.Pointer(sg) sg.success = false if raceenabled { raceacquireg(gp, c.raceaddr()) } glist.push(gp) }
for { sg := c.sendq.dequeue() if sg == nil { break } sg.elem = nil if sg.releasetime != 0 { sg.releasetime = cputicks() } gp := sg.g gp.param = unsafe.Pointer(sg) sg.success = false if raceenabled { raceacquireg(gp, c.raceaddr()) } glist.push(gp) } unlock(&c.lock)
for !glist.empty() { gp := glist.pop() gp.schedlink = 0 goready(gp, 3) } }
|
问题
1.go 可以从一个关闭的channel里面读取数据吗?
可以,Go 语言可以从一个已经关闭的 channel 中读取数据。
具体情况如下:
读取已存在的数据: 当一个 channel 被关闭后,你仍然可以从中读取之前发送到 channel 中但尚未被接收的数据。一旦 channel 中所有已存在的数据都被读取完毕,后续的读取操作将会立即返回该 channel 类型的零值,并且不会阻塞。
判断 channel 是否关闭: 从 channel 接收数据时,可以使用两个返回值的形式来判断 channel 是否已经关闭:
value
:接收到的值。如果 channel 已关闭且没有数据,则为该类型的零值。
- ok: 布尔值。
- 如果
ok
为 true
,表示成功从 channel 接收到了一个值。
- 如果
ok
为 false
,表示 channel 已经被关闭且没有更多数据可读取。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| package main
import ( "fmt" "time" )
func main() { ch := make(chan int, 3)
ch <- 1 ch <- 2 ch <- 3
close(ch)
for i := 0; i < 5; i++ { value, ok := <-ch fmt.Printf("Value: %d, OK: %t\n", value, ok) time.Sleep(time.Millisecond * 100) }
ch2 := make(chan string, 2) ch2 <- "hello" ch2 <- "world" close(ch2) for v := range ch2 { fmt.Println("Range:", v) } }
|
输出:
1 2 3 4 5 6 7
| Value: 1, OK: true Value: 2, OK: true Value: 3, OK: true Value: 0, OK: false Value: 0, OK: false Range: hello Range: world
|
总结:
- 关闭 channel 后,仍然可以读取其中已有的数据。
- 使用
value, ok := <-ch
可以判断 channel 是否关闭以及是否还有数据。
- 使用
range
循环可以方便地读取 channel 中的数据,直到 channel 关闭。
需要注意:
- 向一个已经关闭的 channel 发送数据会导致 panic。
- 关闭一个未初始化的 channel (nil channel) 也会导致 panic。
- 重复关闭一个 channel 也会导致 panic。
操作 |
nil channel |
closed channel |
not nil, not closed channel |
close |
panic |
panic |
正常关闭 |
读 <- ch |
阻塞 |
读到对应类型的零值 |
阻塞或正常读取数据。缓冲型 channel 为空或非缓冲型 channel 没有等待发送者时会阻塞 |
写 ch <- |
阻塞 |
panic |
阻塞或正常写入数据。非缓冲型 channel 没有等待接收者或缓冲型 channel buf 满时会被阻塞 |
因此,在操作 channel 时,需要谨慎处理关闭操作,确保不会出现上述 panic 情况。 通常,由发送方负责关闭 channel。
2.关于如何优雅关闭通道:如何优雅地关闭通道
参考链接
1.Go 程序员面试笔试宝典
2.《Go学习笔记》
3.Go 语言设计与实现
4.《如何优雅地关闭通道》