共计 6369 个字符,预计需要花费 16 分钟才能阅读完成。
channel 概述
Hello 大家好!咱们又见面了,本文咱们一起搞懂 Go 语言中 channel 及 channel 底层实现和一些常见面试题。
channel 是 Go 语言内建的 first-class 类型,也是 Go 语言不同凡响的个性之一。先看一个利用场景,比方协程 A 执行过程中须要创立子协程 A1、A2 … An,协程 A 创立完子协程后就期待子协程退出,这样场景的 Go 为咱们提供三种解决方案:
- 应用 channel 管制子协程
- waitGroup 信号量机制管制子协程
- Context 应用上下文管制子协程
它们三种解决方案各有优劣,比方:应用 channel 来管制子协程长处实现简略,毛病就是当须要大量创立协程时就须要有雷同数量的 channel,这样对于子协程持续派生进去的协程就不不便管制。
首先,想一想,为什么 Go 引入 channel,及 channel 能为咱们提供解决怎么样的问题?理解 channel 能够从 CSP 模型理解,CSP 模型是 Tony Hoare 在 1978 年发表的论文中,CSP 次要讲一种并发编程语言,CSP 容许应用过程组建来形容零碎,它们独立运行,并且只通过消息传递的形式通信。这篇论文是对 Go 创始人 Rob Pike 对 Go 语言并发设计的产生微小影响,最初通过引入 channel 这种新的类型,来实现 CSP 的思维。
在应用 channel 类型时候,你无需引入某个包,就能应用它,它就是 Go 语言内置的类型,不像其余库,你必须的引入 sync 包货 atomic 包能力应用它们。
channel 根本用法
channel 很多人常说所谓的 通道,那么通道也是咱们生存中相似的管道,用来传输货色。计算机能够应用通道来进行通信,在 Go 语言中,常见的容许 Goroutine 之间进行数据传输。在传输中,你须要明确一些规定,首先,每个通道只容许替换指定类型的数据,也称为通道元素类型(相似生存中,自家水管只容许运输能喝的水,运输汽油,你须要应用另一个管道)。在 Go 语言中,应用 chan 关键字来申明一个新通道,应用 close() 函数来敞开通道。
定义好通道,能够往 channel 发送数据,从 channel 中接收数据,你还能够定义 只能承受 、 只能发送 、 也能够承受又能够发送 三种类型。
申明通道类型格局如下:
var 变量 chan 元素类型
例子
var ch1 chan int // 申明一个传递整型的通道
var ch3 chan []int // 申明一个传递 int 切片的通道
创立 channel:
var ch chan string
fmt.Println(ch) // 输入:<nil>
注:通道是援用类型,通道类型的空值是 nil。
申明的通道后须要应用 make 函数初始化之后能力应用,以 channel 的缓冲区大小也是可选的。
func main() {
// 初始化通道,缓冲区大小为 2
ch := make(chan int,2)
ch <- 1
ch <- 2
ch <- 3 // 会报错,因为缓冲区只容许大小为 2
x1 := <- ch
x2:= <- ch
fmt.Println(x1)
fmt.Println(x2)
}
(1)发送数据
往 chan 中发送一个数据应用 ch <-
,格局如下:
ch <- 1 // 把 1 发送到 ch 中
(2)接收数据
从 chan 中接管一条数据应用<-ch
,接收数据也是一条语句,课时如下:
x := <- ch // 从 ch 中接管只并赋值给变量 x1。<- ch // 从 ch 中接管值,疏忽后果
注:对于敞开通道须要留神的事件是,只有在告诉接管方 goroutine 所有的数据都发送结束的时候才须要敞开通道。通道是能够被垃圾回收机制回收的,它和敞开文件是不一样的,在完结操作之后敞开文件是必须要做的,但敞开通道不是必须的。
channel 实现原理
这节,面试官会问:channel 的底层实现?
接下来,一起学习 chan 的数据结构及初始化,还有三个最重要操作方法分为是 send
、recv
和 close
,认真学习 channel 底层原理的实现。
源码目录地位:runtime/chan.go
,以下贴出 chan 类型的数据结构如下:
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 //chan 中元素大小
closed uint32 // 是否曾经 close
elemtype *_type // element type(chan 元素类型)sendx uint // send index(send 在 buf 中索引)recvx uint // receive index(recv 在 buf 中索引)recvq waitq // list of recv waiters(receive 的期待队列)sendq waitq // list of send waiters(sender 期待队列)// 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 // 互斥锁,爱护所有字段,下面正文曾经讲得十分明确了
}
(1)chan 初始化
Go 在编译时,会依据容量大小抉择调用 makechan64,还是 makechan。通过源码咱们能够晓得,makechan64 只做了 size 查看,而后底层最终还是调用 makechan 实现的。(makechan 的指标就是生成 hchan 对象)
Makechan 到底做了什么,源码如下:
func makechan(t *chantype, size int) *hchan {
elem := t.elem
// 编译器会查看类型是否平安
// compiler checks this but be safe.
if elem.size >= 1<<16 {// 是否 >= 2^16
throw("makechan: invalid channel element type")
}
if hchanSize%maxAlign != 0 || elem.align > maxAlign {throw("makechan: bad alignment")
}
mem, overflow := math.MulUintptr(elem.size, uintptr(size))
if overflow || mem > maxAlloc-hchanSize || size < 0 {panic(plainError("makechan: size out of range"))
}
var c *hchan
switch {
case mem == 0:
// chan 的 size 或元素的 size 为 0,就不用创立 buf
c = (*hchan)(mallocgc(hchanSize, nil, true))
// 竞争检测器应用此地位进行同步
c.buf = c.raceaddr()
case elem.ptrdata == 0:
// 元素不是指针,调配一块间断的内存给 hchan 数据结构和 buf
c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
// hchan 数据结构前面紧接着就是 buf
c.buf = add(unsafe.Pointer(c), hchanSize)
default:
// Elements contain pointers.
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
}
总结:channel 底层是依据不同容量和元素类型,来调配不同的对象来初始化 chan 对象的字段,及返回 hchan 对象。
(2)send 办法
send() 是往 chan 发送数据,办法大抵分为 6 个局部,源码如下:
第一局部:
func chansend1(c *hchan, elem unsafe.Pointer) {chansend(c, elem, true, getcallerpc())
}
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
// 第一局部
if c == nil {
if !block {return false}
// 阻塞休眠
gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
throw("unreachable")
}
// 局部代码省略。。。}
send 最开始,首先是判断,如果 chan 为 nil 的话,调用 gopark 进行阻塞休眠,这时,调用者永远阻塞住了。那么这个代码 throw("unreachable")
不会执行的。
第二局部:
// 第二局部,如果 chan 没有被 close, 并且 chan 满了,间接返回
if !block && c.closed == 0 && full(c) {return false}
第三局部:
// 第三局部,chan 曾经被 close 的情景
lock(&c.lock)
if c.closed != 0 {unlock(&c.lock)
panic(plainError("send on closed channel"))
}
第三局部,如果 chan 曾经被 close,你再往里面发送数据的话会呈现 Panic。如下代码会呈现 Panic。
ch := make(chan int,1)
close(ch)
ch <- 1
第四局部:
// 第四局部:如果有 recvq 接收者就阐明 buf 中没数据,因而间接从 sender 送到 receizver 中
if sg := c.recvq.dequeue(); sg != nil {
// Found a waiting receiver. We pass the value we want to send
// directly to the receiver, bypassing the channel buffer (if any).
send(c, sg, ep, func() {unlock(&c.lock) }, 3)
return true
}
第五局部:
// 第五局部,buf 还没满
if c.qcount < c.dataqsiz {
// Space is available in the channel buffer. Enqueue the element to send.
qp := chanbuf(c, c.sendx)
if raceenabled {racenotify(c, c.sendx, nil)
}
typedmemmove(c.elemtype, qp, ep)
c.sendx++
if c.sendx == c.dataqsiz {c.sendx = 0}
c.qcount++
unlock(&c.lock)
return true
}
第五局部阐明以后没有 receiver,须要把数据放入到 buf 中,放入之后,就胜利返回了。
第六局部:
// 第六局部:buf 已满
//chansend1 不会进入 if 块里,因为 chansend1 的 block=true
if !block {unlock(&c.lock)
return false
}
第六局部是解决 buf 满的状况。如果 buf 满了,发送者的 goroutine 就会退出到发送者的期待队列中,直到被唤醒。这个时候,数据或者被取走了,或者 chan 被 close 了。
(2)recv
在解决从 chan 中接收数据,源码如下:
第一局部:
if c == nil { // 判断 chan 为 nil
if !block {return}
gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
throw("unreachable")
}
从 chan 获取数据时,如果 chan 为 nil,调用者会被永远阻塞。
第二局部:
// 第二局部, block=false 且 c 为空
if !block && empty(c) {......}
第三局部:
lock(&c.lock)// 加锁,返回时开释锁
// 第三局部,c 曾经被 close, 且 chan 为空 empty
if c.closed != 0 && c.qcount == 0 {
if raceenabled {raceacquire(c.raceaddr())
}
unlock(&c.lock)
if ep != nil {typedmemclr(c.elemtype, ep)
}
return true, false
}
如果 chan 曾经被 close 了,并且队列中没有缓存的元素,那么返回 true、false。
第四局部:
// 第四局部,如果 sendq 队列中有期待发送的 sender
if sg := c.sendq.dequeue(); sg != nil {
// Found a waiting sender. If buffer is size 0, receive value
// directly from sender. Otherwise, receive from head of queue
// and add sender's value to the tail of the queue (both map to
// the same buffer slot because the queue is full).
recv(c, sg, ep, func() {unlock(&c.lock) }, 3)
return true, true
}
当解决 buf 满的状况。这个时候,如果是 unbuffer 的 chan,就间接将 sender 的数据复制给 receiver,否则就从队列头部读取一个值,并把这个 sender 的值退出到队列尾部。
第五局部:解决没有期待的 sender 的状况。这个是和 chansend 共用一把大锁,所以不会有并发的问题。如果 buf 有元素,就取出一个元素给 receiver。
第六局部:解决 buf 中没有元素的状况。如果没有元素,那么以后的 receiver 就会被阻塞,直到它从 sender 中接管了数据,或者是 chan 被 close,才返回。
(3)close
通过 close 函数,你能够把 chan 敞开,底层调用 closechan 办法执行。具体源码和下面两个地位一样。
(4)应用 channel 踩的坑
常见的谬误 panic 和 goroutine 透露
示例 1:
ch := make(chan int,1)
close(ch)
ch <- 1
往 chan 增加,但 close 了,会呈现 Panic,解决就是不 close。
示例 2:
ch := make(chan int,1)
ch <- 1
close(ch)
<- ch
close(ch)
从 chan 取出数据,但 close 了,也会 Panic
(5)介绍 panic 和 recover
Panic 和 recover 也是面试点,简略留神
Panic:在 Go 语言中,呈现 Panic 是代表一个重大问题,意味着程序完结并退出。在 Go 中 Panic 关键字用于抛出异样的。相似 Java 中的 throw。
recover:在 Go 语言中,用于将程序状态呈现严重错误复原到失常状态。当 产生 Panic 后,你须要应用 recover 捕捉,不捕捉程序会退出。相似 Java 的 try catch 捕捉异样。
你的每次 点赞 + 珍藏 + 关注,是我创作最大能源。加油,奋斗永远都在路上!