Go面试看这里了~(二十八)

161 篇文章 12 订阅

原文地址:Go面试看这里了~(二十八)

1、channel数据结构?

channel管道类型,可在goroutine间发送/接收消息,是Go在语言层面提供的goroutine间的通信方式,主要用于进程内各goroutine间通信,如需跨进程通信,建议使用分布式系统的方式来解决。

来看src/runtime/chan.go文件下hchan的源码,如下:

   type hchan struct {
       qcount   uint           // total data in the queue 当前队列里还剩余元素个数
       dataqsiz uint           // size of the circular queue 环形队列长度,即缓冲区的大小,即make(chan T,N) 中的N
       buf      unsafe.Pointer // points to an array of dataqsiz elements 环形队列指针
       elemsize uint16 //每个元素的大小
       closed   uint32 //标识当前通道是否处于关闭状态,创建通道后,该字段设置0,即打开通道;通道调用close将其设置为1,通道关闭
       elemtype *_type // element type 元素类型,用于数据传递过程中的赋值
       sendx    uint   // send index 环形缓冲区的状态字段,它只是缓冲区的当前索引-支持数组,它可以从中发送数据
       recvx    uint   // receive index 环形缓冲区的状态字段,它只是缓冲区当前索引-支持数组,它可以从中接受数据
       recvq    waitq  // list of recv waiters 等待读消息的goroutine队列
       sendq    waitq  // list of send waiters 等待写消息的goroutine队列
    
       // 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 //互斥锁,为每个读写操作锁定通道,因为发送和接受必须是互斥操作
  }
  
  // sudog 代表goroutine
   type waitq struct {
        first *sudog
        last  *sudog
  }

可看出channel由队列、类型信息、goroutine等待队列等组成,channel内部实现一个环形队列作为其缓冲区,队列长度是创建channel时指定。

来看可缓存6个元素的channel示意图:

参数解释如下:

  1. dataqsiz表示队列长度为6,即可缓存6个元素。

  2. buf指向队列的内存,队列中还剩余两个元素。

  3. qcount表示队列中还有两个元素。

  4. sendx指示后续写入的数据存储的位置,取值[0, 6)。

  5. recvx指示从该位置读取数据, 取值[0, 6)。

从channel读数据,如channel缓冲区为空或没有缓冲区,当前goroutine会被阻塞,向channel写数据,如channel缓冲区已满或没有缓冲区,当前goroutine会被阻塞。

被阻塞goroutine将挂在channel等待队列,因读阻塞goroutine会被向channel写入数据的goroutine唤醒,因写阻塞goroutine会被从channel读数据的goroutine唤醒。

来看无缓冲区channel,有几个goroutine阻塞等待读数据的状态图:

一般情况下recvq和sendq至少有一个为空,只有一个例外,那就是同一个goroutine使用select语句向channel一边写数据,一边读数据。

2、channel类型信息?

在类型信息方面来说,一个channel只能传递一种类型的值,类型信息存储在hchan数据结构中,elemtype代表类型,用于数据传递过程中的赋值,elemsize代表类型大小,用于在buf中定位元素位置。

3、channel锁?

因为channel数据结构的互斥锁,一个channel同时仅允许被一个goroutine读写。

4、channel创建&读写?

创建channel的过程实际上是初始化hchan结构,其中类型信息和缓冲区长度由make语句传入,buf的大小则与元素大小和缓冲区长度共同决定,来看伪代码:

func makechan(t *chantype, size int) *hchan {
  var c *hchan
  c = new(hchan)
  c.buf = malloc(元素类型大小*size)
  c.elemsize = 元素类型大小
  c.elemtype = 元素类型
  c.dataqsiz = size
  return c
}

向一个channel中写数据过程如下:

  1. 如果等待接收队列recvq不为空,说明缓冲区中无数据或无缓冲区,此时直接从recvq取出G,并把数据写入,最后把该G唤醒,结束发送过程。

  2. 如果缓冲区中有空余位置,将数据写入缓冲区,结束发送过程。

  3. 如果缓冲区中没有空余位置,将待发送数据写入G,将当前G加入sendq,进入睡眠,等待被读goroutine唤醒。

操作流程图如下:

从一个channel读数据过程如下:

  1. 如果等待发送队列sendq不为空,且无缓冲区,直接从sendq中取出G,把G中数据读出,最后把G唤醒,结束读取过程。

  2. 如果等待发送队列sendq不为空,此时说明缓冲区已满,从缓冲区中首部读出数据,把G中数据写入缓冲区尾部,把G唤醒,结束读取过程。

  3. 如果缓冲区中有数据,则从缓冲区取出数据,结束读取过程。

  4. 将当前goroutine加入recvq,进入睡眠,等待被写goroutine唤醒。

操作流程图如下:

5、channel关闭?

关闭channel时会将recvq中的G全部唤醒,本该写入G的数据位置为nil,把sendq中的G全部唤醒,但这些G会panic,panic出现的常见场景如下:

  1. 关闭值为nil的channel。

  2. 关闭已经被关闭的channel。

  3. 向已经关闭的channel写数据。

6、单向channel?

单向channel指只用于发送/接收数据,实际上并无单向channel,channel可以通过参数传递,所谓单向channel只是对channel的一种使用限制,跟C用const修饰函数参数为只读道理一样,来看个代码案例:

func readChan(chanName <-chan int) {
    <- chanName
}

func writeChan(chanName chan<- int) {
    chanName <- 1
}

func main() {
    var mychan = make(chan int, 10)

    writeChan(mychan)
    readChan(mychan)
}

上述代码中的func readChan(chanName <-chan int)通过形参限定函数内部只能从channel中读取数据,func writeChan(chanName chan<- int) 通过形参限定函数内部只能向channel中写入数据,mychan是个正常的channel,而readChan()参数限制了传入的channel只能用来读,writeChan()参数限制了传入的channel只能用来写。

7、select、range监控读取channel?

可用select可以监控多channel,如监控多个channel,当其中一个channel有数据时,就从其读出数据,来看个代码案例:

package main

import (
    "fmt"
    "time"
)

func addNumberToChan(chanName chan int) {
    for {
        chanName <- 1
        time.Sleep(1 * time.Second)
    }
}

func main() {
    var chan1 = make(chan int, 10)
    var chan2 = make(chan int, 10)

    go addNumberToChan(chan1)
    go addNumberToChan(chan2)

    for {
        select {
        case e := <- chan1 :
            fmt.Printf("Get element from chan1: %d\n", e)
        case e := <- chan2 :
            fmt.Printf("Get element from chan2: %d\n", e)
        default:
            fmt.Printf("No element in chan1 and chan2.\n")
            time.Sleep(1 * time.Second)
        }
    }
}

上述代码中创建两个channel,分别为chan1和chan2,函数addNumberToChan()函数会向两个channel中周期性写入数据,通过select可监控两个channel,任意一个可读时就从其中读出数据,输出内容如下:

由上图输出内容可知,从channel中读出数据的顺序是随机的,事实上select语句的多个case执行顺序是随机的,主要想说明的就是,select的case语句读channel不会阻塞,尽管channel中没有数据,这是由于case编译后调用读channel时会明确传入不阻塞的参数,此时读不到数据时不会将当前goroutine加入到等待队列,而是直接返回。

还可通过range持续从channel中读出数据,像在遍历一个数组一样,当channel中没有数据时会阻塞当前goroutine,与读channel时阻塞处理机制一样,如向此channel写数据的goroutine退出时,系统检测到这种情况后会panic,否则range将会永久阻塞。

代码示例如下:

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)
}

输出内容如下:

死锁是指2个或2个以上goroutine在执行任务过程中,由于竞争资源或彼此通信而造成的一种阻塞现象,在非缓冲信道如发生只流入不流出或者只流出不流入就会发生死锁。

至此,本次分享就结束了,后期会慢慢补充。

以上仅为个人观点,不一定准确,能帮到各位那是最好的。

好啦,到这里本文就结束了,喜欢的话就来个三连击吧。

扫码关注公众号,获取更多优质内容。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

luyaran

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值