乐趣区

关于golang:彻底明白Go语言的Channel了

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 的数据结构及初始化,还有三个最重要操作方法分为是 sendrecvclose,认真学习 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 捕捉异样。

你的每次 点赞 + 珍藏 + 关注,是我创作最大能源。加油,奋斗永远都在路上!

退出移动版