一、并发#
1 GMP模型——goroutine调度#
1.1 GMP优点#
GMP模型可以在用户空间实现任务的切换,上下文切换成本更小,可以达到使用较少的线程数量实现较大并发的能力
1.2 GMP含义#
G是goroutine,是golang协程,是用户态轻量级线程。
M是machine,是内核级线程,是实质上实现业务逻辑的载体。
P是processor本地队列
1.3 M必须拥有P,才能执行G中的代码,P负责G的调度。#
1.4 如何新增G#
M1新增G会被保存在M1所绑定的本地队列P1中,P1队列超过256个满了以后,会把新增的G放进全局队列中
1.5 M如何从P中获取G#
从本地队列P中获取G(无锁)
如果本地队列为空,从全局队列中获取G(加锁)
如果全局队列也为空,再去另一个本地队列P中偷一半的G
1.6 当M1在执行G1的时候被阻塞了,如何继续执行#
当M1在执行G1的时候阻塞了,M1与绑定的本地队列P1解绑,接着M2绑定P1,然后执行P1的下一个协程G2。
1.7 G的生命周期#
- 创建G:go func()可以创建协程G
- 保存G:创建的G优先保存到本地队列P,如果本地队列P满了,则会放到全局队列P中
- M获取G:M1首先从本地队列P1获取G,如果P1为空,则从全局队列中获取G,如果全局队列也为空,则从另一个本地队列偷一半的G
- M调度和执行G:M1调用G.func()函数执行协程G,如果M1在执行G的过程被阻塞了,则本地队列P1与M1解绑。其他M绑定P1,继续执行P1的其他G。
1.8 GM和GMP的区别#
- 减少大量的全局队列锁竞争。M 绑定本地队列 P 后,直接在 P 中获取、添加和执行 G(无锁操作)。
- 尽量在同 1 个 M 中创建和执行 G。因为 G 的信息在创建时保存到 M 中,所以在后续执行的过程中不需要转移 G 的信息,即不涉及线程上下文切换。
1.9 M和P的数量#
- P的个数在程序启动时决定,默认情况下等同于CPU的核数。
- P的数量一般大于M的数量
- M创建的条件
没有足够的M来绑定P。
比如所有的M此时都阻塞了,但是 P 中还有很多就绪任务,就要去寻找空闲 M,没找到空闲的 M 就会去创建新的 M。
2 Golang协程切换时机#
- 会阻塞的系统调用,比如文件io,网络io
- time系列定时操作
- 协程执行完成
- 管道读写阻塞
- 垃圾回收之后
- 主动调用
3 如何在Golang中对性能优化#
- 使用 goroutine 和 channel 实现并发
- 使用 sync 包中的锁,但是尽量减少锁的使用,可以优先考虑读写锁。
- 使用原子操作
4 Golang中线程同步的方法#
- channel:用于在goroutine之间传递数据和同步操作。
- WaitGroup:用于等待一组 goroutine 执行完成后再进行下一步操作。
- Mutex 和 RWMutex:用于保护共享资源,避免多个 goroutine 同时访问。
- Atomic:用于对共享资源进行原子操作。
二、内存#
1 GC垃圾回收:三色标记法和混合写屏障#
1.1 三色标记法#
- 在最开始,所有对象的颜色设置成白色
- 从根节点开始遍历所有对象,将遍历到的对象放进灰色集合
- 遍历灰色集合,将灰色对象引用的对象变为灰色,遍历之后将本对象标记为黑色
- 重复第三步,直到灰色中无任何对象
- 回收所有白色对象。
1.2 混合写屏障#
- GC开始将栈上的对象全部扫描并标记为黑色,之后不再进行第二次重复扫描
- GC期间,任何在栈上创建的新对象均为黑色
- 被删除的对象标记为灰色
- 被添加的对象标记为灰色
1.8版本后为了不造成stop the world,提高回收精度混合写屏障满足弱三色不变式,只需要在开始时并发扫描各个goroutine的栈,使其变黑并一直保持,不需要STW。因为栈在扫描后始终是黑色的,也不需要进行re-scan操作。
强三色不变式#
不存在黑色对象引用到白色对象的指针。
弱三色不变式#
黑色对象可以引用白色对象,但前提是白色对象存在其他灰色对象对它的引用,或链路上游存在灰色对象。
1.3 GC触发时机#
- 主动触发
调用runtime.GC,阻塞式地等待当前 GC 运行完毕 - 被动触发
- 距上一次GC的最长时间,默认两分钟
- 分配的堆大小达到阈值
golang内存模型#
只有使用原子库、互斥锁、channel,才能保证在不同的Goroutines间安全地共享数据。
三、数据结构和关键字#
1 mutex#
mutex很像操作系统中的PV操作,通过信号量来处理线程中同步与互斥的问题。
S代表剩余资源数。
P代表申请资源,S原子减一,如果减一后S小于0则将自己阻塞起来。
V表示释放资源,S原子++,如果S++后S<=0,表示等待队列上有等待线程,需要将第一个等待的线程唤醒。
- mutex是一个结构体,提供lock()和unlock()方法。
- state代表互斥锁的状态,例如是否被锁定。内部实现分成四部分
- sema表示信号量,等待信号量的协程会阻塞,解锁的协程释放信号量从而唤醒等待信号量的协程
2 channel#
- channel是一个用于通信的管道,遵循先进先出。
- 需要用make来初始化channel,可以选择是否有缓冲,无缓冲是同步的。
channel的底层实现#
- 缓冲区是个循环队列,保存队列当前的大小,和队列最大大小
- 发送者队列和接收者队列,用于存储等待写入或读取数据的 goroutine 的信息;
- 互斥锁
如果往一个关闭的channel读、写会怎么样#
- 如果写的话,会直接panic。所以永远不要在读端关闭channel,多个写端可以通过context来解决。
- 如果里边有数据,会拿到数据
- 如果里边没有数据,会读到零值,可以通过判断ok为false来解决。
使用channel的注意事项#
- channel关闭后的读写问题
- 无缓冲的channel是同步的,避免阻塞
- 有缓冲的channel的容量如果满了,发送方会阻塞
对map的理解,map有哪些注意事项#
- map是引用类型,将map赋值、传递的时候,用的是引用,而不是内容。
- map不是线程安全的,可以用互斥锁或者sync.Map
- map的底层实现使用哈希表来存储元素,迭代顺序不一定
- 对map切片会指向原内存空间,可能出现并发或者内存泄漏问题。通过申请新的内存空间做拷贝来解决
- 当元素数量达到容量2/3触发自动扩容,容量翻倍。创建新哈希表进行计算、拷贝等,释放旧哈希表内存。
- 需要使用ok来判断
当向 map 中存储一个 kv 时,
通过 k 的 hash 值与 buckets 长度取余,定位到 key 在哪一个bucket中,
hash 值的高8位存储在 bucket 的 tophash[i] 中,用来快速判断 key是否存在。
当一个 bucket 满时,通过 overflow 指针链接到下一个 bucket。
对slice的理解#
slice和数组的区别#
- 数组(array)的长度是固定的,切片(slice)是可变的
- 数组是值类型,切片是引用类型。拷贝数组会复制整个数组的内容,复制切片只能复制指针
- 数组是连续的内存块,切片是通过指针和长度来实现的。
slice扩容规则是什么#
- 如果当前切片长度小于1024,新容量为原来的2倍
- 当前切片长度大于等于1024,新容量为原来的1.25倍
- 如果一次扩容后仍无法容纳新增元素,会继续扩容(加法和对齐)
- 扩容时会将原来的元素复制到新的内存空间,旧的被释放。
context#
1 context的作用#
- 在gorountine之间传递上下文信息。一般用来:超时控制,并发控制。
- context的树形结构可以在不同层级的goroutine之间有效的传递信号。
2 context树的结构#
- 树的根是一个空的context(context.Background()或者context.TODO())
- 需要某个节点是子操作,只需要在声明ctx的时候把父ctx传进去
- 每个节点代表一个新创建的context,可能包含值、取消信号或截止时间
- 树的边代表从父context到子context的继承关系
3 取消操作#
- 当需要取消一个操作和所有子操作的时候,通过调用树中某个节点的cancel()函数实现。
- 触发沿树向下传递的取消信号,使所有子节点都能收到取消信号。
4 context树的构建#
- context.WithValue,包含键值对,在整个请求范围内传递数据
- context.WithCancle、context.WithDeadline、context.WithTimeout,附加取消信号或截止时间,信号用于在请求被取消或超时时通知子goroutine
new和make的区别#
- new用于基本数据类型和结构体类型的内存分配,置为初始值
- make适用于引用类型的内存分配,如切片、map和channel等
其他#
1 如何在Golang中对性能优化#
- 使用 Golang 的内置工具进行性能分析
- 减少内存分配。尽可能地重用变量和对象。
- 减少垃圾收集,减少内存分配也有助于减少垃圾收集。
- 使用并发处理。
- 避免使用过多的锁,可以使用读写锁来减少锁的使用
- 使用适当的数据结构
- 使用编译器优化。
golang中常见的引发panic的情况#
- 往被关闭的channel写数据
- 关闭已经被关闭的channel
- 类型断言失败,可以通过判断是否断言成功来避免
- 数组下标越界
- 空指针调用
- 除数是0