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)发送数据
ch <-
ch <- 1 //把1发送到ch中
(2)接收数据
<-ch
x := <- ch //从ch中接管只并赋值给变量x1。
<- ch //从ch中接管值,疏忽后果
注:对于敞开通道须要留神的事件是,只有在告诉接管方goroutine所有的数据都发送结束的时候才须要敞开通道。通道是能够被垃圾回收机制回收的,它和敞开文件是不一样的,在完结操作之后敞开文件是必须要做的,但敞开通道不是必须的。
channel 实现原理
这节,面试官会问:channel 的底层实现?
sendrecvclose
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 //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")
}
// 局部代码省略。。。
}
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 捕捉异样。
你的每次 点赞+珍藏+关注,是我创作最大能源。加油,奋斗永远都在路上!