跳过正文
  1. 面试题库/

03|Channel 相关

·2387 字·5 分钟
目录
Golang面试题库 - 这篇文章属于一个选集。
§ 3: 本文

1. channel 是否线程安全?锁用在什么地方?
#

分析

channel配合goroutine可以用来实现并发编程,并且是go语言推荐的并发编程模式,那么肯定是可以保证线程安全的,可以先回顾下channel的底层定义,channel用make函数创建初始化的时候会在堆上分配一个runtime.hchan类型的数据结构

type hchan struct {
    qcount    uint           // 循环队列中的元素总数
    dataqsiz  uint           // 循环队列大小
    buf       unsafe.Pointer // 指向循环队列的指针
    elemsize  uint16         // 循环队列中的每个元素的大小
    closed    uint32         // 标记位,标记channel是否关闭
    elemtype  *_type         // 循环队列中的元素类型
    sendx     uint           // 已发送元素在循环队列中的索引位置
    recvx     uint           // 已接收元素在循环队列中的索引位置
    recvq     waitq          // 等待从channel接收消息的sudog队列
    sendq     waitq          // 等待向channel写入消息的sudog队列
    lock      mutex          // 互斥锁,对channel的数据读写操作加锁,保证并发安全
}

可以看到channel的底层实现中是有锁的,是通过mutex来保证线程安全的,所以在回答的时候要突出底层实现有锁

回答

一般来说,我们对channel就只有读、写、关闭三种操作,这三种操作,channel底层数据结构都用同一把runtime.Mutex来进行保护

2. channel 的底层实现原理(数据结构)
#

分析

这个问题其实是上一个问题的补充,channel的底层实现是一个hchan的结构,hchan的结构定义回顾这个图

hchan结构图

type hchan struct {
    qcount    uint           // channel 环形数组中的素数量
    dataqsiz  uint           // channel 环形数组的容量
    buf       unsafe.Pointer // 指向Channel 环形数组的一个指针
    elemsize  uint16         // 元素类型的字节数
    closed    uint32         // 是否关闭
    elemtype  *_type         // 元素类型
    sendx     uint           // send index 下一次的位置
    recvx     uint           // receive index 下一次的位置
    recvq     waitq          // list of recv waiters 接等待队列
    sendq     waitq          // list of send waiters 等待队列
    lock      mutex          // runtime.mutex, 保证channel并发安全
}

回答

  1. 对于包含缓冲的channel,go语言的channel底层是一个hchan的结构,里面包含一个指向循环数组的指针,这个循环数组是用于存储数据的,当然还包含下次读取和下次发送的数据最引位置recvx和sendx

  2. 还包含两个goroutine等待队列,在一个goroutine对这个channel读写阻塞的时候会分流放到这两个队列里,发送数据阻塞就放到sendq这个等待队列,接收数据阻塞就放到recvq这个等待队列

  3. 为了保证channel的线程安全,hchan结构还有一个互斥锁,用作数据读写时候加锁,当然close channel也会用到这个互斥锁

3. nil、关闭的channel、有数据的channel,再进行读、写、关闭会怎么样?(各类变种题型)
#

分析

主要是考察对channel在各个状态下进行读写操作会出现什么结果,这块建议自己代码跑一下各个场景,加深一下理解

回答

操作 \ 状态未初始化(nil)关闭(closed)正常(normal)
关闭panicpanic正常关闭
当前goroutine永久性挂起,可能导致死锁panic阻塞挂起或者成功发送
当前goroutine永久性挂起,可能导致死锁读取缓冲区数据,读完后,每次读取都返回零值阻塞挂起或者成功接收
  1. 对nil的channel进行读和写 都会造成当前goroutine永久阻塞(如果当前goroutine是main goroutine,则会让整个程序直接报fatal error 退出,也就是报错deadlock),关闭则会发生panic

  2. 对已经关闭的channel进行写 和 再次关闭,都会导致panic,而读操作的话,会一直将channel中的数据读完,读完之后,每次读channel都会获得一个对应类型的零值

  3. 对一个正常的channel进行读写有两种情况

    a. 读:阻塞挂起或者成功发送

    b. 写:阻塞挂起或者成功接收

    c. 关闭:正常close

4. 对channel 进行读写数据的流程是怎样的
#

分析

考察对channel 底层结构以及chansend和chanrecv流程的掌握程度,下面回答不区分有缓冲channel 和 无缓冲channel,注意理解

下面是对一个非nil,且未关闭的channel进行读写的流程

回答

操作一个不为nil,并且未关闭的channel,读和写都有两种情况

  1. 读操作:

    a. 成功读取:

    如果channel中有数据,直接从channel里面读取,并且此时如果写等待队列里面有goroutine,还需要将队列头部goroutine数据放入到channel中,并唤醒这个goroutine

    如果channel没有数据,就尝试从 写等待队列 头部goroutine读取数据,并做对应的唤醒操作

    b. 阻塞挂起: channel里面没有数据 并且 写等待队列为空,则将当前goroutine 加入 读等待队列中,并挂起,等待唤醒

  2. 写操作

    a. 成功写入:

    如果channel 读等待队列不为空,则取 头部goroutine,将数据直接复制给这个头部goroutine,并将其唤醒,流程结束

    否则就会尝试将数据写入到channel 环形缓冲中

    b. 阻塞挂起: 通道里面无法存放数据 并且 读等待队列为空,则当前goroutine 加入 写等待队列中,并挂起,等待唤醒

5. select的底层原理
#

分析

select也被称为多路select,指的是一个goroutine 可以服务多个 channel的读或写操作,要清楚的知道 select分为两种,包含非阻塞型select(包含default分支的) 和 阻塞型select(不包含default分支的),然后再回答其对应原理

回答

  • select的核心原理是,按照随机的顺序执行case,直到某个case完成操作,如果所有case的都没有完成操作,则看有没有default分支,如果有default分支,则直接走default,防止阻塞

  • 如果没有的话,需要将当前goroutine 加入到所有case对应channel的等待队列中,并挂起当前goroutine,等待唤醒。

  • 如果当前goroutine被某一个case 上的channel操作唤醒后,还需要将当前goroutine从所有case对应channel的等待队列中剔除

Golang面试题库 - 这篇文章属于一个选集。
§ 3: 本文