go语言设计与实现-读书笔记

作者: dino.ma 分类: Golang 发布时间: 2020-08-03 15:00

Go语言设计与实现-读书笔记 原书地址

编译原理

  1. 静态单赋值(Static Single Assignment, SSA)是中间代码的一个特性,如果一个中间代码具有静态单赋值的特性,那么每个变量就只会被赋值一次。
  2. 常数传播(constant propagation)
  3. 值域传播(value range propagation)
  4. 稀疏有条件的常数传播(sparse conditional constant propagation)
  5. 消除无用的程式码(dead code elimination)
  6. 全域数值编号(global value numbering)
  7. 消除部分的冗余(partial redundancy elimination)
  8. 强度折减(strength reduction)
  9. 寄存器分配(register allocation
  10. 抽象语法树(AST),是源代码语法的结构的一种抽象表示,它用树状的方式表示编程语言的语法结构。抽象语法树中的每一个节点都表示源代码中的一个元素,每一颗子树都表示一个语法元素,
  11. 计算机指令集
  12. 复杂指令集:复杂指令集通过增加指令的数量减少需要执行的指令数;
  13. 精简指令集:精简指令集能使用更少的指令完成目标的计算任务;
    4.编译原理核心过程
  14. 词法与语法分析(解析源码后转换成token序列并按照语言定义好的文法将每一个go的源文件形成一个SourceFile结构)
  15. 将token转换为一个有意义的结构体,也就是一个AST抽象语法树
  16. 类型检查
  17. 对make进行改写 makechan makeslice makemap
  18. 并发编译 goroutine

数据结构

  1. 数组
  2. [..]int{},[22]int{} 在编译期间会做推倒,如果元素小于等于4个 则在栈上,如果大于则放置到静态区在运行时(Runtime)取出
  3. 数组在初始化之后就无法改变其大小。
  4. 两个数组大小相同、类型相同才为同一类型
  5. 数组在底层是一片连续的内存空间,在发生扩容的时候,是通过汇编的memmove进行内存的拷贝(提前计算好需要占用的内存空间)
  6. memmove 当内存发生重叠时,保证结果一定是正确的。
  7. memcopy 当内存发生重叠时,不保证结果一定是正确的。
  8. 切片
  9. 切片可以立即为是一个动态的数组,可以自动做扩容。
  10. length指的是当前切片的长度,cap指的是切片对应底层数组的容量(也可以理解为是底层数组的大小)
  11. 如果期望容量大于当前容量的两倍就会使用期望容量;如果当前切片的长度小于 1024 就会将容量翻倍;如果当前切片的长度大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;(但是实际扩容的内存空间,因为为了提高内容使用效率底层使用内存对其具体的还是参考运行时的gc部分)
  12. 需要注意的是在遇到大切片扩容或者复制时可能会发生大规模的内存拷贝,一定要在使用时减少这种情况的发生避免对程序的性能造成影响。在做类型转换时也会有较大的开销,如可避免则尽量避免(可以在工程优化时做一下处理,参考火焰图)
  13. 哈希表
    1. 数组用于表示元素的序列,而哈希表示的是键值对之间映射关系,只是不同语言的叫法和实现稍微有些不同。
    2. 哈希函数
    1.在理想情况下,哈希函数应该能够将不同键映射到不同的索引上,这要求哈希函数输出范围大于输入范围,但是由于键的数量会远远大于映射的范围,所以在实际使用时,这个理想的结果是不可能实现的。只能通过工程手段去解决哈希碰撞的问题,让哈希的结果尽可能的均匀。
        1.开放寻址法(此时支撑哈希表的数据结构为数组,但是数组长度有限)
            1.对数组中的元素依次探测和比较以判断目标键值对是否存在于哈希表中。index:= hash("key") % array.len 在写数据时会写入到下一个不为空的位置。如键存在值不同则会做更新。如果装载因子(数组元素数量/数组大小)超过70%则性能极具下降,如超过100%则哈希表完全失效。变为O(n)
        2.拉链法(链表数组:动态申请内存、可扩展、节省内存空间)
            1.实现拉链法一般会使用数组加上链表,不过有一些语言会在拉链法的哈希中引入红黑树以优化性能,拉链法会使用链表数组作为哈希底层的数据结构,我们可以将它看成一个可以扩展的『二维数组』:
            2.写入数据时通过 index:= hash("key")%array.len选择对应的桶Bmap(TopHash uint8一级缓存)
                1.找到键相同的键值对 —— 更新键对应的值;
                2.没有找到键相同的键值对 —— 在链表的末尾追加新键值对;
                3.装载因子 := 元素数量 / 桶数量(当装载因子大于6.5 或者有过多的溢出桶时会出发增量扩容)
3.  go语言通过Hmap和Bmap去描述哈希表和桶
    1.  每个Bmap只存储8个键值对(TopHash uint8 可以理解为是一级缓存加速检索)
    2.  哈希表小于等于25个元素时会直接压入哈希表,否则会调整为两个数组,在变异期间循环加入目标哈希表。
    3.  Hmap中有一个随机种子(fastrand)用于在for range 为哈希表的遍历引入不确定性,也是告诉所有使用 Go 语言的使用者,程序不要依赖于哈希表的稳定遍历。
    4.  在判断hash表中的key是否存在时,必须使用 v,ok := map["key"] 而不要使用零值进行判断
    5.  哈希表的扩容非原子操作(写入时做增量扩容和等量扩容,通过gc+)
        1.  当装载因子大于6.5 或者有过多的溢出桶时会出发增量扩容
        2.  通过运行时的mapassign进行判断当前哈希表是否在扩容过程中,避免二次扩容造成混乱
        3.  运行时的evacDst会做新、旧桶数据分流
        4.  哈希在存储元素过多时会触发扩容操作,每次都会将桶的数量翻倍,整个扩容过程并不是原子的,而是通过 runtime.growWork 增量触发的,在扩容期间访问哈希表时会使用旧桶,向哈希表写入数据时会触发旧桶元素的分流;除了这种正常的扩容之外,为了解决大量写入、删除造成的内存泄漏问题,哈希引入了 sameSizeGrow 这一机制,在出现较多溢出桶时会对哈希进行『内存整理』减少对空间的占用。
        5.  扩容的时候会做双倍扩容,写时做增量扩容不会造成性能的瞬时巨大抖动。
  1. 字符串
    1. 字符串的底层实现与切片相似。
    2. 字符串的解析在词法分析时解析完毕
    3. 字符串在运行时做字符串拼接,A+B+C 会在计算所需占用内存空间后单独去申请一片 string D的空间变成一个新字符串,新字符串与原字符串5⃣无任何关联。一旦拼接的字符串比较大则发生内存拷贝带来的性能损失时没办法忽略的。
    4. string byte等做类型转换性能开销极大,
    5. 参考golang 原生json库的性能 与滴滴老板的jsoniter

语言基础

  1. 函数调用
    1. 函数调用均为值拷贝(如果值过大在做内存拷贝的时候会有性能问题)无论是传递基本类型、结构体还是指针,都会对传递的参数进行拷贝。
    2. 通过堆栈传递参数,入栈的顺序是从右到左;
    3. 函数返回值通过堆栈传递并由调用者预先分配内存空间;
    4. 调用函数时都是传值,接收方会对入参进行复制再计算;
  2. 接口
    1. 代码必须能够被人阅读,只是机器恰好可以执行《计算机程序的构造和解释》
    2. 实现接口的所有方法就隐式的实现了接口;
    3. Go语言中的接口都是隐式的
    4. 接口在做调用的时候会做隐式转换,转换为Interface{}
  3. 反射
    1. reflect 包为我们提供的多种能力,包括如何使用反射来动态修改变量、判断类型是否实现了某些接口以及动态调用方法等功能

常用关键字

  1. for和range
    1. 在遍历哈希表的时候通过获取的fastrand的随机种子让哈希表遍历变得无序,程序不要依赖哈希表的稳定遍历
    2. 在遍历中 v 始终为一个内存地址。如果取其地址做赋值则均为循环最后一个值
  2. select
    1. select的流程控制必须为channel的收发操作
    2. select 能在 Channel 上进行非阻塞的收发操作;
    3. select 在遇到多个 Channel 同时响应时会随机挑选 case 执行;
  3. defer
    1. goroutine _defer的链表 去执行defer
  4. panic和recover
    1. panic 只会触发当前 Goroutine 的延迟函数调用(defer)
    2. recover并不包含程序恢复逻辑,实际上是通过recover的调用去改变_panic.recovered的参数为true
  5. make和new
    1. make初始化go语言提供的内置数据结构(channel,slice,map),new根据传入的类型分配一片内存空间并返回指向这片内存地空间的指针
    2. 类型检查阶段会将make替换为makeslice makechan makemap

并发编程(必会)

  1. 上下文Context
    1. 类似于用户token,trace-id可以放在Context中,但是避免通过Context做数据透传
    2. context.Context 可以在上层 Goroutine 执行出现错误时,将信号及时同步给下层。
    3. Go 语言中的 context.Context 的主要作用还是在多个 Goroutine 组成的树中同步取消信号以减少对资源的消耗和占用,虽然它也有传值的功能,但是这个功能我们还是很少用到。
  2. 同步原语与锁
    1. mutex 互斥锁 ,获取锁的时间大于1ms则将当前goroutine变更为饥饿模式,如果获取锁的间隔在1ms之内则会变更为正常模式
      1. 相比于饥饿模式,正常模式下的互斥锁能够提供更好地性能,饥饿模式的能避免 Goroutine 由于陷入等待无法获取锁而造成的高尾延时。
      2. 加锁
        1. 互斥锁只有在普通模式下才可以进入自旋,向cpu发送30次pause(只占用cpu和cpu时间),在多核cpu上可以避免goroutine进行切换
        2. 当前Goroutine为了获取锁而自旋的次数小于4次
        3. 当前机器上至少存在一个正在运行的处理器 P 并且处理的运行队列为空;
    2. RWMutex读写互斥锁(基于互斥锁)
      1. 读写互斥锁 sync.RWMutex 是细粒度的互斥锁,它不限制资源的并发读,但是读写、写写操作无法并行执行。
      2. 获取读锁时先阻塞写锁的获取,后阻塞读锁的获取,这种策略能够保证读操作不会被连续的写操作『饿死』。
    3. WaitGroup
      1. sync.WaitGroup 可以等待一组 Goroutine 的返回,一个比较常见的使用场景是批量发出 RPC 或者 HTTP 请求,我们可以通过 sync.WaitGroup 将原本顺序执行的代码在多个 Goroutine 中并发执行,加快程序处理的速度。
      2. sync.WaitGroup 必须在 sync.WaitGroup.Wait 方法返回之后才能被重新使用;
      3. sync.WaitGroup.Done 只是对 sync.WaitGroup.Add 方法的简单封装,我们可以向 sync.WaitGroup.Add 方法传入任意负数(需要保证计数器非负)快速将计数器归零以唤醒其他等待的 Goroutine;
      4. 可以同时有多个 Goroutine 等待当前 sync.WaitGroup 计数器的归零,这些 Goroutine 会被同时唤醒;
    4. Once
      1. Go 语言标准库中 sync.Once 可以保证在 Go 程序运行期间的某段代码只会执行一次。
      2. 作为用于保证函数执行次数的 sync.Once 结构体,它使用互斥锁和 sync/atomic 包提供的方法实现了某个函数在程序运行期间只能执行一次的语义。
      3. sync.Once.Do 方法中传入的函数只会被执行一次,哪怕函数中发生了 panic;
      4. 两次调用 sync.Once.Do 方法传入不同的函数也只会执行第一次调用的函数;
    5. Confd 很有用哦~
      1. Go 语言标准库中的 sync.Cond 一个条件变量,它可以让一系列的 Goroutine 都在满足特定条件时被唤醒。
      2. 可以理解为多个goroutine执行时需要等待confd做一个广播去唤醒全部Goroutine或者最前面的Goroutine。
      3. sync.Cond 不是一个常用的同步机制,在遇到长时间条件无法满足时,与使用 for {} 进行忙碌等待相比,sync.Cond 能够让出处理器的使用权(P)。
    6. ErrGroup (与waitGroup类似)
      1. 如果返回错误 — 这一组 Goroutine 最少返回一个错误;如果返回空值 — 所有 Goroutine 都成功执行;
      2. x/sync/errgroup.Group 在出现错误或者等待结束后都会调用 context.Context 的 cancel 方法同步取消信号;
      3. 只有第一个出现的错误才会被返回,剩余的错误都会被直接抛弃;
    7. semaphore(信号量)
      1. 信号量是在并发编程中常见的一种同步机制,在需要控制访问资源的进程数量时就会用到信号量,它会保证持有的计数器在 0 到初始化的权重之间波动。
      2. x/sync/semaphore.Weighted.Acquire 和 x/sync/semaphore.Weighted.TryAcquire 方法都可以用于获取资源,前者会阻塞地获取信号量,后者会非阻塞地获取信号量;
      3. x/sync/semaphore.Weighted.Release 方法会按照 FIFO (先进先出)的顺序唤醒可以被唤醒的 Goroutine;
      4. 如果一个 Goroutine 获取了较多地资源,由于 x/sync/semaphore.Weighted.Release 的释放策略可能会等待比较长的时间;
    8. SingleFlight (贼棒)
      1. 是 Go 语言扩展包中提供了另一种同步原语,它能够在一个服务中抑制对下游的多次重复请求。一个比较常见的使用场景是 — 我们在使用 Redis 对数据库中的数据进行缓存,发生缓存击穿时,大量的流量都会打到数据库上进而影响服务的尾延时
      2. 当我们需要减少对下游的相同请求时,就可以使用 x/sync/singleflight.Group 来增加吞吐量和服务质量,不过在使用的过程中我们也需要注意以下的几个问题:
        1. x/sync/singleflight.Group.Do 和 x/sync/singleflight.Group.DoChan 一个用于同步阻塞调用传入的函数,一个用于异步调用传入的参数并通过 Channel 接收函数的返回值;
        2. x/sync/singleflight.Group.Forget 方法可以通知 x/sync/singleflight.Group 在持有的映射表中删除某个键,接下来对该键的调用就不会等待前面的函数返回了;
        3. 一旦调用的函数返回了错误,所有在等待的 Goroutine 也都会接收到同样的错误;
  3. 定时器
    1. Go 1.10 ~ 1.13,全局使用 64 个四叉堆维护全部的计时器,每个处理器(P)创建的计时器会由对应的四叉堆维护
    2. Go 1.14 版本之后,每个处理器单独管理计时器并通过网络轮询器触发
    3. Go 语言的计时器在并发编程起到了非常重要的作用,它能够为我们提供比较准确的相对时间,基于它的功能,标准库中还提供了定时器、休眠等接口能够我们在 Go 语言程序中更好地处理过期和超时等问题。
    4. 标准库中的计时器在大多数情况下是能够正常工作并且高效完成任务的,但是在遇到极端情况或者性能敏感场景时,它可能没有办法胜任,而在 10ms 的这个粒度下,作者在社区中也没有找到能够使用的计时器实现,一些使用时间轮算法的开源库也不能很好地完成这个任务。
  4. Channel(祖宗)
    1. 不要通过共享内存的方式进行通信,而是应该通过通信的方式共享内存。(Go语言协程通信的特性。)
    2. Goroutine是用户态线程不被计算机系统感知,所以才会有调度器的产生。
    3. 计算机最小操作单位为进程,线程是程序执行的最小单位,协程为用户态线程。
    4. Go的并发模型为通信顺序进程(Communicating sequential processes,CSP)
    5. Channel FIFO 数据先入先出
    6. 同步 Channel — 不需要缓冲区,发送方会直接将数据交给(Handoff)接收方;如果接收方不在,当前Goroutine会被阻塞
    7. 异步 Channel — 基于环形缓存的传统生产者消费者模型;
    8. chan struct{} 类型的异步 Channel — struct{} 类型不占用内存空间,不需要实现缓冲区和直接发送(Handoff)的语义;
    9. ch:=make(chan int) 在编译期间会被替换为runtime.makechan or runtime.makechan64
      1. 如果当前 Channel 中不存在缓冲区,那么就只会为 runtime.hchan 分配一段内存空间;
      2. 如果当前 Channel 中存储的类型不是指针类型,就会为当前的 Channel 和底层的数组分配一块连续的内存空间;
      3. 在默认情况下会单独为 runtime.hchan 和缓冲区分配内存
    10. 使用 ch <- i 表达式向 Channel 发送数据时遇到的几种情况
      10.1 如果当前 Channel 的 recvq 上存在已经被阻塞的 Goroutine,那么会直接将数据发送给当前的 Goroutine 并将其设置成下一个运行的 Goroutine;
      10.2 如果 Channel 存在缓冲区并且其中还有空闲的容量,我们就会直接将数据直接存储到当前缓冲区 sendx 所在的位置上;
      10.3 如果不满足上面的两种情况,就会创建一个 runtime.sudog 结构并将其加入 Channel 的 sendq 队列中,当前 Goroutine 也会陷入阻塞等待其他的协程从 Channel 接收数据;
      10.4 发送数据的过程中包含2个会触发 Goroutine 调度的时机:
      10.4.1 发送数据时发现 Channel 上存在等待接收数据的 Goroutine,立刻设置处理器的 runnext 属性,但是并不会立刻触发调度;
      10.4.2 发送数据时并没有找到接收方并且缓冲区已经满了,这时就会将自己加入 Channel 的 sendq 队列并调用 runtime.goparkunlock 触发 Goroutine 的调度让出处理器的使用权;
    11. 使用 ch <- 表达式从Channel接收数据会遇到五种情况
      10.1 如果 Channel 为空,那么就会直接调用 runtime.gopark 挂起当前 Goroutine;(这个其实就是比较高明的地方,不会占用处理器P)
      10.1.1 当存在等待的发送者时,通过 runtime.recv 直接从阻塞的发送者或者缓冲区中获取数据;
      10.1.2 当缓冲区存在数据时,从 Channel 的缓冲区中接收数据;
      10.1.3 当缓冲区中不存在数据时,等待其他 Goroutine 向 Channel 发送数据;
      10.2 如果 Channel 已经关闭并且缓冲区没有任何数据,runtime.chanrecv 函数会直接返回;
      10.3 如果 Channel 的 sendq 队列中存在挂起的 Goroutine,就会将 recvx 索引所在的数据拷贝到接收变量所在的内存空间上并将 sendq 队列中 Goroutine 的数据拷贝到缓冲区;
      10.4 如果 Channel 的缓冲区中包含数据就会直接读取 recvx 索引对应的数据;
      10.5 在默认情况下会挂起当前的 Goroutine,将 runtime.sudog 结构加入 recvq 队列并陷入休眠等待调度器的唤醒;
      10.6 从 Channel 接收数据时,会触发 Goroutine 调度的两个时机:
      10.6.1 当 Channel 为空时;
      10.6.2 当缓冲区中不存在数据并且也不存在数据的发送者时;
    12. 关闭channel
      1. 当 Channel 是一个空指针或者已经被关闭时,Go 语言运行时都会直接 panic 并抛出异常。该函数在最后会为所有被阻塞的 Goroutine 调用 runtime.goready 触发调度。可以参考源码:closechan
  5. 调度器(核心)
    5.1 进程与线程
    5.1.1 多个线程可以在一个进程内共享一片连续的内存空间,每个线程占用1mb以上的内存空间,在线程切换时会消耗较多的内存空间、每一次线程的上下文切换都会消耗~1us的时间。而Goroutine在切换时因为是用户态线程,Go的调度器对Goroutine的上下文切换大概为~0.2us,性能为线程切换的~5倍。
    5.2 线程与Goroutine(高明之处)
    5.2.1 Go语言的调度器默认使用的线程数等于系统cpu核数,减少cpu切换的内存开销等。同时在每个线程上使用更轻量的Gorotine来更好的压榨cpu性能。
    5.3 GMP的前世今生
    5.3.1 单线程调度器,程序中只存在一个活跃的线程。由G-M模型组成
    5.3.2 多线程调度器,程序中存在多个活跃的线程,锁竞争严重。由G-M模型组成
    5.3.3 任务窃取调度器,在某些情况下程序中的Goroutine不会主动让出线程造城【饥饿】问题。时间过长的垃圾回收(Stop the world STW)最长可能需要几分钟的时间会造成程序长时间无法工作,参考java的垃圾回收(同理)。引入处理器p的基础上实现了过做窃取调度器,此时的模型由G-M-P组成
    5.3.4 抢占式调度器 1.2 ~ 至今
    5.3.4.1 基于协作的抢占式调度器 1.2~1.13。通过编译器在函数调用时插入【抢占检查】指令,在函数调用时检查当前Goroutine是否发起了抢占请求,实现了基于协作的抢占式调度。但是带来的问题是:Goroutine可能会因为垃圾回收和循环长时间占用资源导致程序暂停。【这个该说不说属实有点差劲】
    5.3.4.2 基于信号的抢占式调度器 1.14~至今。实现基于信号的真抢占式调度。垃圾回收在扫描栈时会触发抢占调度。基于信号的抢占式调度只解决了垃圾回收和栈扫描时存在的问题。将_Gorunnine状态的Goroutine标记为可抢占,并且触发抢占,向当前和其他线程发送信号执行抢占。
    5.4 GMP的数据结构
    5.4.1 G — 表示 Goroutine,它是一个待执行的任务;用户态线程,可以配合调度器更好的去压榨cpu。
    5.4.2 M — 表示操作系统的线程,它由操作系统的调度器调度和管理;
    5.4.3 P — 表示处理器,它可以被看做运行在线程上的本地调度器;
    5.5 调度器启动
    5.5.1 runtime.schedinit初始化调度器,默认最大线程数为10000(maxmcount)。可同时运行线程数(M)默认为系统内核数。
    5.6 创建Goroutine
    5.6.1 获取Goroutine的三种方式
    5.6.1.1 当处理器的(P) Goroutine 列表为空时,会将调度器(sched.gFree)持有的空闲 Goroutine 转移到当前处理器上,直到 gFree 列表中的 Goroutine 数量达到 32
    5.6.1.2 当处理器的 Goroutine 数量充足时,会从列表头部返回一个新的 Goroutine;
    5.6.1.3 当调度器的 sched.gFree 和处理器的 p.gFree 列表都不存在结构体时,运行时会调用 runtime.malg 初始化一个新的 runtime.g 结构体,如果申请的堆栈大小大于 0,在这里我们会通过 runtime.stackalloc 分配 2KB 的栈空间: 5.6.2 全局和本地运行队列 5.6.2.1 处理器本地的运行队列是一个使用数组构成的环形链表,它最多可以存储 256 个待执行任务。 5.6.2.2 处理器本地的运行队列,调度器持有的全局运行队列,只有在本地运行队列没有剩余空间时才会使用全局队列。 5.7 调度循环
    5.7.1 Go 语言中的运行时调度循环会从 runtime.schedule 函数开始,最终又回到 runtime.schedule;这里介绍的是 Goroutine 正常执行并退出的逻辑,实际情况会复杂得多,多数情况下 Goroutine 的执行的过程中都会经历协作式或者抢占式调度,这时会让出线程的使用权等待调度器的唤醒。
    5.8 触发调度
    5.8.1 主动让出(runtime.gopark)、系统调用(runtime.exitsyscall)、协作式调度(runtime.Gosched -)、系统监控(runtime.sysmon)
    5.9 线程管理
    5.9.1 Go 语言的运行时会通过调度器改变线程的所有权,它也提供了 runtime.LockOSThread 和 runtime.UnlockOSThread 让我们有能力绑定 Goroutine 和线程完成一些比较特殊的操作。Goroutine 应该在调用操作系统服务或者依赖线程状态的非 Go 语言库时调用 runtime.LockOSThread 函数11,
  6. 网络轮询器
    6.1 三种io模型(阻塞 I/O 模型;非阻塞 I/O 模型;I/O 多路复用模型;)
    6.2 select 最多监听1024个事件。它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。
    6.3 poll poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的.
    6.4 epoll epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))
  7. 系统监控
    7.1 设计原理
    7.1.1 Go 语言的系统监控也起到了很重要的作用,它在内部启动了一个不会中止的循环,在循环的内部会轮询网络、抢占长期运行或者处于系统调用的 Goroutine 以及触发垃圾回收,通过这些行为,它能够让系统的运行状态变得更健康。
    7.2 监控循环(当程序趋于稳定之后,系统监控的触发时间就会稳定在 10ms。)
    7.2.1 检查死锁;
    7.2.1.1 检查是否存在正在运行的线程;
    7.2.1.2 检查是否存在正在运行的 Goroutine;
    7.2.1.3 检查处理器上是否存在计时器;
    7.2.2 运行计时器 – 获取下一个需要被触发的计时器;
    7.2.2.1 我们发现下一个计时器需要触发的时间小于当前时间,这也就说明所有的线程可能正在忙于运行 Goroutine,系统监控会启动新的线程来触发计时器,避免计时器的到期时间有较大的偏差。(这有一个疑问?默认m等于cpu核数那么这个线程哪里来的?)
    7.2.3 抢占处理器 — 抢占运行时间较长的或者处于系统调用的 Goroutine;
    7.2.3.1 系统监控通过在循环中抢占处理器来避免同一个 Goroutine 占用线程太长时间造成饥饿问题。
    7.2.3.1.1 当处理器处于 _Prunning 或者 _Psyscall 状态时,如果上一次触发调度的时间已经过去了 10ms,我们就会通过 runtime.preemptone 抢占当前处理器;
    7.2.3.1.2 当处理器处于 _Psyscall 状态时,在满足以下两种情况下会调用 runtime.handoffp 让出处理器的使用权:当处理器的运行队列不为空或者不存在空闲处理器时; 当系统调用时间超过了 10ms 时;
    7.2.4 垃圾回收 — 在满足条件时触发垃圾收集回收内存;
    7.2.4.1 系统监控还会决定是否需要触发强制垃圾回收,runtime.sysmon 会构建 runtime.gcTrigger 结构体并调用 runtime.gcTrigger.test 函数判断是否需要触发垃圾回收.如果需要触发垃圾回收,我们会将用于垃圾回收的 Goroutine 加入全局队列,让调度器选择合适的处理器去执行。
    7.2.5 轮询网络 — 获取需要处理的到期文件描述符;

内存管理

  1. 内存分配器
  2. 垃圾收集器
  3. 栈内存管理

我的博客即将搬运同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan

发表评论

电子邮件地址不会被公开。 必填项已用*标注