导图社区 Golang
golang面试题,GC-三色标记STW:写屏障:避免这类误清扫问题. 写屏障即在内存写操作前,维护一个约束,从而确保清扫开始前,黑色的对象不能引用白色对象。
编辑于2022-02-15 17:12:57goroutine思维导图,本文整理了 1、底层实现原理? 2、goroutine和线程的区别? 3、goroutine泄漏的场景? 4、如何查看正在执行的goroutine数量? 5、如何控制并发的goroutine数量? 6、什么时候抢占P? 7、关于启动流程? 8、goroutine挂起? 相关知识可以导出使用。
Mutex主要整理了1、互斥锁的实现原理;2、正常模式和饥饿模式的区别;3、互斥锁允许自旋的条件;4、读写锁的实现原理;5、可重入锁如何实现;6、原子操作有哪些?7、原子操作和锁的区别 七个方面。
MySQL面试题:聚簇索引:数据和索引存储到一起,找到索引就获取到了数据。聚簇索引是唯一的,InnoDB一定会有一个聚簇索引来保存数据。非聚簇索引一定存储有聚簇索引的列值。
社区模板帮助中心,点此进入>>
goroutine思维导图,本文整理了 1、底层实现原理? 2、goroutine和线程的区别? 3、goroutine泄漏的场景? 4、如何查看正在执行的goroutine数量? 5、如何控制并发的goroutine数量? 6、什么时候抢占P? 7、关于启动流程? 8、goroutine挂起? 相关知识可以导出使用。
Mutex主要整理了1、互斥锁的实现原理;2、正常模式和饥饿模式的区别;3、互斥锁允许自旋的条件;4、读写锁的实现原理;5、可重入锁如何实现;6、原子操作有哪些?7、原子操作和锁的区别 七个方面。
MySQL面试题:聚簇索引:数据和索引存储到一起,找到索引就获取到了数据。聚簇索引是唯一的,InnoDB一定会有一个聚簇索引来保存数据。非聚簇索引一定存储有聚簇索引的列值。
Golang
GC-三色标记STW
三色标记法
分类:
* 白色对象(可能死亡):未被回收器访问到的对象。在回收开始阶段,所有对象均为白色,当回收结束后,白色对象均不可达。 * 灰色对象(波面):已被回收器访问到的对象,但回收器需要对其中的一个或多个指针进行扫描,因为他们可能还指向白色对象。 * 黑色对象(确定存活):已被回收器访问到的对象,其中所有字段都已被扫描,黑色对象中任何一个指针都不可能直接指向白色对象。
标记过程
1. 起初所有的对象都是白色的; 2. 从根对象出发扫描所有可达对象,标记为灰色,放入待处理队列; 3. 从待处理队列中取出灰色对象,将其引用的对象标记为灰色并放入待处理队列中,自身标记为黑色; 4. 重复步骤3,直到待处理队列为空,此时白色对象即为不可达的“垃圾”,回收白色对象;
观察GC
方式1:GODEBUG=gctrace=1 方式2:go tool trace go tool trace 的主要功能是将统计而来的信息以一种可视化的方式展示给用户。要使用此工具,可以通过调用 trace 方式3:debug.ReadGCStats 方式4:runtime.ReadMemStats
GC触发机制
* 主动触发(手动触发),通过调用runtime.GC 来触发GC,此调用阻塞式地等待当前GC运行完毕. * 被动触发,分为两种方式: a. 使用系统监控,当超过2min没有产生任何GC时,强制触发 GC. b. 使用步调(Pacing)算法,其核心思想是控制内存增长的比例,当前内存分配达到一定比例则触发.
写屏障
避免这类误清扫问题. 写屏障即在内存写操作前,维护一个约束,从而确保清扫开始前,黑色的对象不能引用白色对象.
各版本演进
* Go 1:串行三色标记清扫 * Go 1.3:并行清扫,标记过程需要 STW,停顿时间在约几百毫秒 * Go 1.5:并发标记清扫,停顿时间在一百毫秒以内 * Go 1.6:使用 bitmap 来记录回收内存的位置,大幅优化垃圾回收器自身消耗的内存,停顿时间在十毫秒以内 * Go 1.7:停顿时间控制在两毫秒以内 * Go 1.8:混合写屏障,停顿时间在半个毫秒左右 * Go 1.9:彻底移除了栈的重扫描过程 * Go 1.12:整合了两个阶段的 Mark Termination,但引入了一个严重的 GC Bug 至今未修(见问题 20),尚无该 Bug 对 GC 性能影响的报告 * Go 1.13:着手解决向操作系统归还内存的,提出了新的 Scavenger * Go 1.14:替代了仅存活了一个版本的 scavenger,全新的页分配器,优化分配内存过程的速率与现有的扩展性问题,并引入了异步抢占,解决了由于密集循环导致的 STW 时间过长的问题
相关API
* runtime.GC:手动触发 GC * runtime.ReadMemStats:读取内存相关的统计信息,其中包含部分 GC 相关的统计信息 * debug.FreeOSMemory:手动将内存归还给操作系统 * debug.ReadGCStats:读取关于 GC 的相关统计信息 * debug.SetGCPercent:设置 GOGC 调步变量 * debug.SetMaxHeap(尚未发布[10]):设置 Go 程序堆的上限值
goroutine
为什么快
* goroutine是用户态线程,其创建和切换都在用户代码中完成而无需进入操作系统内核,所以其开销要远远小于系统线程的创建和切换; * goroutine启动时默认栈大小只有2k,这在多数情况下已经够用了,即使不够用,goroutine的栈也会自动扩大,同时,如果栈太大了过于浪费它还能自动收缩,这样既没有栈溢出的风险,也不会造成栈内存空间的大量浪费。
goroutine挂起
通道Channel
垃圾回收GC
休眠Sleep
锁等待Lock
抢占Preempted
IO阻塞IO Wait
其他:panic、finalizer、select
goroutine泄漏的情况有哪些
*Goroutine 内正在进行 channel/mutex 等读写操作,但由于逻辑问题,某些情况下会被一直阻塞。 *Goroutine 内的业务逻辑进入死循环,资源一直无法释放。 *Goroutine 内的业务逻辑进入长时间等待,有不断新增的 Goroutine 进入等待。
为什么要有P
什么时候抢占P
1.如果存在系统调用超时:存在超过 1 个 sysmon tick 周期(至少 20us)的任务,则会从系统调用中抢占 P。 2.如果没有空闲的 P:所有的 P 都已经与 M 绑定。需要抢占当前正处于系统调用之,而实际上系统调用并不需要的这个 P 的情况,会将其分配给其它 M 去调度其它 G。 3.如果 P 的运行队列里面有等待运行的 G,为了保证 P 的本地队列中的 G 得到及时调度。而自己本身的 P 又忙于系统调用,无暇管理。此时会寻找另外一个 M 来接管 P,从而实现继续调度 G 的目的。
关于启动流程
启动流程
g0
g 一般分为三种,分别是: 执行用户任务的叫做 g。 执行 runtime.main 的 main goroutine。 执行调度任务的叫 g0。。 g0 比较特殊,每一个 m 都只有一个 g0(仅此只有一个 g0),且每个 m 都只会绑定一个 g0。在 g0 的赋值上也是通过汇编赋值的,其余后续所创建的都是常规的 g。 从多个方面来看: 数据结构:g0 和其他创建的 g 在数据结构上是一样的,但是存在栈的差别。在 g0 上的栈分配的是系统栈,在 Linux 上栈大小默认固定 8MB,不能扩缩容。而常规的 g 起始只有 2KB,可扩容。 运行状态:g0 和常规的 g 不一样,没有那么多种运行状态,也不会被调度程序抢占,调度本身就是在 g0 上运行的。 变量声明:g0 和常规 g,g0 的定义就是 var g0 g,没什么特别之处。
m0
m0 是 Go Runtime 所创建的第一个系统线程,一个 Go 进程只有一个 m0,也叫主线程。 从多个方面来看: 数据结构:m0 和其他创建的 m 没有任何区别。 创建过程:m0 是进程在启动时应该汇编直接复制给 m0 的,其他后续的 m 则都是 Go Runtime 内自行创建的。 变量声明:m0 和常规 m 一样,m0 的定义就是 var m0 m,没什么特别之处。
数量控制在多少合适
结构
stack
stack结构体主要用来记录goroutine所使用的栈的信息,包括栈顶和栈底位置:
gobuf
gobuf结构体用于保存goroutine的调度信息,主要包括CPU的几个寄存器的值:
g
g结构体用于代表一个goroutine,该结构体保存了goroutine的所有信息,包括栈,gobuf结构体和其它的一些状态信息:
理论上会受内存的影响
m
m结构体用来代表工作线程,它保存了m自身使用的栈信息,当前正在运行的goroutine以及与m绑定的p等信息
M 的默认数量限制是 10000
debug.SetMaxThreads 方法进行设置。
p
p结构体用于保存工作线程执行go代码时所必需的资源,比如goroutine的运行队列,内存分配用到的缓存等等。
P 的数量受环境变量 GOMAXPROCS 的直接影响。
schedt
schedt结构体用来保存调度器的状态信息和goroutine的全局运行队列:
调度模型
调度流程
1.当我们执行 go func() 时,实际上就是创建一个全新的 Goroutine,我们称它为 G。
2.新创建的 G 会被放入 P 的本地队列(Local Queue)或全局队列(Global Queue)中,准备下一步的动作。需要注意的一点,这里的 P 指的是创建 G 的 P。
3.唤醒或创建 M 以便执行 G。
4.不断地进行事件循环
5.寻找在可用状态下的 G 进行执行任务
6.清除后,重新进入事件循环
本地队列有数量限制,不允许超过 256 个。 并且在新建 G 时,会优先选择 P 的本地队列,如果本地队列满了,则将 P 的本地队列的一半的 G 移动到全局队列。 这可以理解为调度资源的共享和再平衡。
基本理解
进程
进程是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。
线程
是进程的一个实体,线程是内核态,而且是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。
协程
是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。 协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
数据结构
slice
数据结构
* 指向底层数组的指针 * 切片的长度 * 切片的容量
扩容策略
* 首先判断,如果新申请容量大于 2 倍的旧容量,最终容量就是新申请的容量 * 否则判断,如果旧切片的长度小于 1024,则最终容量就是旧容量的两倍 * 否则判断,如果旧切片长度大于等于 1024,则最终容量从旧容量开始循环增加原来的 1/4, 直到最终容量大于等于新申请的容量 * 如果最终容量计算值溢出,则最终容量就是新申请容量
sync.map
数据结构
// 封装的线程安全的map type Map struct { // lock mu Mutex // 实际是readOnly这个结构 // 一个只读的数据结构,因为只读,所以不会有读写冲突。 // readOnly包含了map的一部分数据,用于并发安全的访问。(冗余,内存换性能) // 访问这一部分不需要锁。 read atomic.Value // readOnly // dirty数据包含当前的map包含的entries,它包含最新的entries(包括read中未删除的数据,虽有冗余,但是提升dirty字段为read的时候非常快,不用一个一个的复制,而是直接将这个数据结构作为read字段的一部分),有些数据还可能没有移动到read字段中。 // 对于dirty的操作需要加锁,因为对它的操作可能会有读写竞争。 // 当dirty为空的时候, 比如初始化或者刚提升完,下一次的写操作会复制read字段中未删除的数据到这个数据中。 dirty map[interface{}]*entry // 当从Map中读取entry的时候,如果read中不包含这个entry,会尝试从dirty中读取,这个时候会将misses加一, // 当misses累积到 dirty的长度的时候, 就会将dirty提升为read,避免从dirty中miss太多次。因为操作dirty需要加锁。 misses int } // readOnly is an immutable struct stored atomically in the Map.read field. type readOnly struct { m map[interface{}]*entry // 如果Map.dirty有些数据不在m中,这个值为true amended bool } // An entry is a slot in the map corresponding to a particular key. type entry struct { // *interface{} p unsafe.Pointer }
map
数据结构
type hmap struct { count int //map元素的个数,调用len()直接返回此值 // map标记: // 1. key和value是否包指针 // 2. 是否正在扩容 // 3. 是否是同样大小的扩容 // 4. 是否正在 `range`方式访问当前的buckets // 5. 是否有 `range`方式访问旧的bucket flags uint8 B uint8 // buckets 的对数 log_2 noverflow uint16 // overflow 的 bucket 近似数 hash0 uint32 // hash种子 计算 key 的哈希的时候会传入哈希函数 buckets unsafe.Pointer // 指向 buckets 数组,大小为 2^B 如果元素个数为0,就为 nil // 扩容的时候,buckets 长度会是 oldbuckets 的两倍 oldbuckets unsafe.Pointer // bucket slice指针,仅当在扩容的时候不为nil nevacuate uintptr // 扩容时已经移到新的map中的bucket数量 extra *mapextra // optional fields }
链表解决哈希冲突
是否线程安全
扩容机制
装载因子超过 6.5
overflow buckets 太多
channel
数据结构
type hchan struct { qcount uint // 当前队列中剩余元素个数 dataqsiz uint // 环形队列长度,即可以存放的元素个数 buf unsafe.Pointer // 环形队列指针 elemsize uint16 // 每个元素的大小 closed uint32 // 标识关闭状态 elemtype *_type // 元素类型 sendx uint // 队列下标,指示元素写入时存放到队列中的位置 recvx uint // 队列下标,指示元素从队列的该位置读出 recvq waitq // 等待读消息的goroutine队列 sendq waitq // 等待写消息的goroutine队列 lock mutex // 互斥锁,chan不允许并发读写 }
线程安全
* channel内部维护了一个互斥锁,来保证线程安全: * Golang的Channel,发送一个数据到Channel 和 从Channel接收一个数据 都是 原子性的。 * 而且Go的设计思想就是:不要通过共享内存来通信,而是通过通信来共享内存,前者就是传统的加锁,后者就是Channel。
使用场景
* channel的能力是让数据流动起来,擅长的是数据流动的场景。 * mutex的能力是数据不动,某段时间只给一个协程访问数据的权限,擅长数据位置固定的场景。
有无缓冲
无缓冲的 channel 是同步的,而有缓冲的 channel 是非同步的。 * channel无缓冲时,发送阻塞直到数据被接收,接收阻塞直到读到数据。 * channel有缓冲时,当缓冲满时发送阻塞,当缓冲空时接收阻塞。
对nil channel的读写会永久block 向closed channel写入会发生panic 从closed channel读取会立即读出零值
context
常用功能
WithValue:值传递
WithTimeout:超时取消
WithDeadline:定时取消
WithCancel:手动取消
无论是超时取消,定时取消还是手动取消,它本质都是往Context的Done通道里面放一个值罢了,我们通过监听Done通道来达到实现这些目的。
使用原则
1. 不要把Context放在结构体中,要以参数的方式传递 2. 以Context作为参数的函数方法,应该把Context作为第一个参数,放在第一位 3. 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO() 4. Context的Value相关方法应该传递必须的参数,不要什么数据都使用这个传递 5. Context是线程安全的,可以放心的在多个goroutine中传递
interface
type和value
defer
defer 在函数内部使用,一般多用在加锁、解锁;打开文件、关闭文件等成对出现的情况
make&new
make:slice、chan、map
创建并初始化内部数据结构(长度,容量)
new
分配内存空间,返回指针引用