Golang八股文
想到什么写什么
协程是什么
协程是一种轻量级用户态线程,不由操作系统的内核管理,协程的创建和调度完全由 Go 调度器管理
GMP 模型
GMP 指 Go 运行时系统的三个组件:Goroutine、Machine、Processor
- Goroutine:go 实现的协程,goroutine 最终要放在 M 上执行
- Machine:操作系统的线程,M 必须和 P 绑定后才能执行 goroutine
- Processor:goroutine 的调度上下文,管理着一组 goroutine 队列,会对队列做一些调度:比如把占用 CPU 时间过长的 goroutine 暂停,运行后续 goroutine;如果自己的队列消费完了就去全局队列里拿;如果全局队列也消费完了就去其他 P 的队列里抢任务。P 反映了最大并行数,所以默认值为 CPU 的核心数
- 当创建一个 goroutine 时,go 的调度器会把这个 goroutine 放到当前 P 的本地队列。如果当前 P 的本地队列满了,Go 会把这个新 G 连同本地队列的一半 G 一起搬到全局队列里
- 之后 M 需要和一个 P 绑定才能工作,他会优先从绑定的 P 的本地队列中取出一个 G 来工作。如果本地队列空了,M 会去全局队列里取;如果本地和全局队列都空了,M 会去从其他的 P 那里取一半的 G 放入到当前 P 的本地队列
- 当 M 成功拿到 G,开始在 CPU 上运行。如果运行完毕,M 会把它放到 P 的空闲列表,清空 G 的资源供下次复用。如果 G 在执行某些阻塞操作,为了不影响 P 本地队列中后面 G 的运行,M 会主动释放 P,P 会与新的 M 绑定,当 M 执行完任务回来之后,他会去绑定新的 P,如果绑定不到的话,就把刚才运行完的 G 丢进全局队列
跟其他语言相比,其他语言的线程是由 OS 内核调度的。goroutine 则是由 Go 运行时调度的,调度器采用 m : n 的技术(调度 m 个 goroutine 到内核线程 / M)。其一大特点是 goroutine 的调度是在用户态下完成的, 不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池, 不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多
defer 执行顺序
- 栈式执行,后进先出,也就是逆序执行
- 参数在
defer声明时就完成求值 - 返回值先赋值,再执行
defer,最后真正返回 - 命名返回值可以被
defer修改(匿名返回值不行,返回值先赋值是拷贝的副本)
1 |
|
slice 切片底层与扩容
1 | type slice struct { |
slice底层结构体由三部分组成,当len要超过cap时会触发扩容机制,创建一个更大新的底层数组并拷贝原数据- 小容量切片扩容以大约2倍增长,大容量切片扩容随容量增大扩容倍率下降
- 扩容后新旧切片不再共享底层数组
map 底层与扩容
map底层基于哈希桶和溢出桶实现,每个桶存放8个键值对,存放项时,先根据key计算哈希值,哈希值的低位决定该键值对落在哪个桶内,哈希值的高8位用于快速比对,决定键值对落在该桶的哪个位置。如果桶满了,会通过链表跳转到溢出桶map原生是并发不安全的,并发读写未加保护可能会panicmap当键值对过多时(每个桶的平均元素 > 6.5)时会触发翻倍扩容;当溢出桶过多时会触发等量扩容(整理)- 翻倍扩容:开辟新内存,桶的数量翻一倍,并记录指向旧桶的指针
- 等量扩容:开辟新内存,桶的数量和原来相同(整理),减少了溢出桶的数量,并记录指向旧桶的指针
- 扩容期间同时存在对新桶和旧桶的引用
- 数据渐进迁移,后续读写时顺带搬迁旧桶的数据,减少一次性抖动
sync.Map 底层和使用场景
sync.Map 是并发安全的
1 | type Map struct { |
- 读取时先从只读层读取,只读层读取是原子读取操作,性能快,无需锁。如果只读层找不到 key,需要加锁去脏数据层读取,并且计数器要 +1
- 写入数据时需要加锁写入脏数据层
- 当
计数器 >= len(dirty)时,认为只读层与脏数据层差别太大,需要把脏数据层的数据拷贝到只读层
综上,选择使用场景如下:
sync.Map适合读多写少,键值对长期稳定的情况。否则更常用map + mutexsync.Map键值对类型是any,如果需要强类型声明需要选择map
CSP
CSP 是 Communicating Sequential Processes,强调通过通信共享内存,而不是通过共享内存通信。CSP 认为并发系统应该由一组独立、顺序运行的实体组成,实体之间通过发送信息来共享数据
在 go 中,实体就是 goroutine,共享数据的通道就是 channel
- goroutine 负责执行,用 channel 负责 goroutine 之间的通信
- 用 channel 通信代替锁,可以减少锁竞争和共享可变状态,减少死锁和数据竞争
- CSP 是 go 语言的一种并发设计思想,不是“完全不用锁”
Channel
channel 底层维护了三个数据结构:
- 环形数组(有缓冲区),维护
sendx和recvz表示下一个发送的元素和下一个接收的接收元素在数组中的索引 - 发送者队列,由双向链表实现,如果缓冲区已满,发送者会进入发送者队列
- 接收者队列,由双向链表实现,如果缓冲区为空,接收者会进入接收者队列
- 往 channel 写数据时,如果接收者队列中有
receiver,说明缓冲区为空,数据直接发送给receiver。如果接收者队列为空,会尝试把数据写入缓冲区。如果缓冲区未满,直接把数据写入缓冲区。如果缓冲区已满,把该sender加入发送者队列。 - 从 channel 读数据时,如果发送者队列中有
sender,说明缓冲区满了,尝试从缓冲区读取数据。如果缓冲区为空(缓冲区大小为0),就从sender获取数据。如果发送者队列中没有sender,先尝试从缓冲区读取数据,如果没有就把该receiver加入接收者队列。
说些什么吧!