bc's club

This is Bc's club

Golang

一、并发#

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的生命周期#

  1. 创建G:go func()可以创建协程G
  2. 保存G:创建的G优先保存到本地队列P,如果本地队列P满了,则会放到全局队列P中
  3. M获取G:M1首先从本地队列P1获取G,如果P1为空,则从全局队列中获取G,如果全局队列也为空,则从另一个本地队列偷一半的G
  4. M调度和执行G:M1调用G.func()函数执行协程G,如果M1在执行G的过程被阻塞了,则本地队列P1与M1解绑。其他M绑定P1,继续执行P1的其他G。

1.8 GM和GMP的区别#

  1. 减少大量的全局队列锁竞争。M 绑定本地队列 P 后,直接在 P 中获取、添加和执行 G(无锁操作)。
  2. 尽量在同 1 个 M 中创建和执行 G。因为 G 的信息在创建时保存到 M 中,所以在后续执行的过程中不需要转移 G 的信息,即不涉及线程上下文切换。

1.9 M和P的数量#

  1. P的个数在程序启动时决定,默认情况下等同于CPU的核数。
  2. P的数量一般大于M的数量
  3. M创建的条件
    没有足够的M来绑定P。
    比如所有的M此时都阻塞了,但是 P 中还有很多就绪任务,就要去寻找空闲 M,没找到空闲的 M 就会去创建新的 M。

2 Golang协程切换时机#

  1. 会阻塞的系统调用,比如文件io,网络io
  2. time系列定时操作
  3. 协程执行完成
  4. 管道读写阻塞
  5. 垃圾回收之后
  6. 主动调用

3 如何在Golang中对性能优化#

  1. 使用 goroutine 和 channel 实现并发
  2. 使用 sync 包中的锁,但是尽量减少锁的使用,可以优先考虑读写锁。
  3. 使用原子操作

4 Golang中线程同步的方法#

  1. channel:用于在goroutine之间传递数据和同步操作。
  2. WaitGroup:用于等待一组 goroutine 执行完成后再进行下一步操作。
  3. Mutex 和 RWMutex:用于保护共享资源,避免多个 goroutine 同时访问。
  4. Atomic:用于对共享资源进行原子操作。

二、内存#

1 GC垃圾回收:三色标记法和混合写屏障#

1.1 三色标记法#

  1. 在最开始,所有对象的颜色设置成白色
  2. 从根节点开始遍历所有对象,将遍历到的对象放进灰色集合
  3. 遍历灰色集合,将灰色对象引用的对象变为灰色,遍历之后将本对象标记为黑色
  4. 重复第三步,直到灰色中无任何对象
  5. 回收所有白色对象。

1.2 混合写屏障#

  1. GC开始将栈上的对象全部扫描并标记为黑色,之后不再进行第二次重复扫描
  2. GC期间,任何在栈上创建的新对象均为黑色
  3. 被删除的对象标记为灰色
  4. 被添加的对象标记为灰色

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,表示等待队列上有等待线程,需要将第一个等待的线程唤醒。

  1. mutex是一个结构体,提供lock()和unlock()方法。
  2. state代表互斥锁的状态,例如是否被锁定。内部实现分成四部分
  3. sema表示信号量,等待信号量的协程会阻塞,解锁的协程释放信号量从而唤醒等待信号量的协程

2 channel#

  • channel是一个用于通信的管道,遵循先进先出。
  • 需要用make来初始化channel,可以选择是否有缓冲,无缓冲是同步的。

channel的底层实现#

  1. 缓冲区是个循环队列,保存队列当前的大小,和队列最大大小
  2. 发送者队列和接收者队列,用于存储等待写入或读取数据的 goroutine 的信息;
  3. 互斥锁

如果往一个关闭的channel读、写会怎么样#

  1. 如果写的话,会直接panic。所以永远不要在读端关闭channel,多个写端可以通过context来解决。
  2. 如果里边有数据,会拿到数据
  3. 如果里边没有数据,会读到零值,可以通过判断ok为false来解决。

使用channel的注意事项#

  1. channel关闭后的读写问题
  2. 无缓冲的channel是同步的,避免阻塞
  3. 有缓冲的channel的容量如果满了,发送方会阻塞

对map的理解,map有哪些注意事项#

  1. map是引用类型,将map赋值、传递的时候,用的是引用,而不是内容。
  2. map不是线程安全的,可以用互斥锁或者sync.Map
  3. map的底层实现使用哈希表来存储元素,迭代顺序不一定
  4. 对map切片会指向原内存空间,可能出现并发或者内存泄漏问题。通过申请新的内存空间做拷贝来解决
  5. 当元素数量达到容量2/3触发自动扩容,容量翻倍。创建新哈希表进行计算、拷贝等,释放旧哈希表内存。
  6. 需要使用ok来判断

当向 map 中存储一个 kv 时,
通过 k 的 hash 值与 buckets 长度取余,定位到 key 在哪一个bucket中,
hash 值的高8位存储在 bucket 的 tophash[i] 中,用来快速判断 key是否存在。
当一个 bucket 满时,通过 overflow 指针链接到下一个 bucket。

对slice的理解#

slice和数组的区别#

  1. 数组(array)的长度是固定的,切片(slice)是可变的
  2. 数组是值类型,切片是引用类型。拷贝数组会复制整个数组的内容,复制切片只能复制指针
  3. 数组是连续的内存块,切片是通过指针和长度来实现的。

slice扩容规则是什么#

  1. 如果当前切片长度小于1024,新容量为原来的2倍
  2. 当前切片长度大于等于1024,新容量为原来的1.25倍
  3. 如果一次扩容后仍无法容纳新增元素,会继续扩容(加法和对齐)
  4. 扩容时会将原来的元素复制到新的内存空间,旧的被释放。

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中对性能优化#

  1. 使用 Golang 的内置工具进行性能分析
  2. 减少内存分配。尽可能地重用变量和对象。
  3. 减少垃圾收集,减少内存分配也有助于减少垃圾收集。
  4. 使用并发处理。
  5. 避免使用过多的锁,可以使用读写锁来减少锁的使用
  6. 使用适当的数据结构
  7. 使用编译器优化。

golang中常见的引发panic的情况#

  1. 往被关闭的channel写数据
  2. 关闭已经被关闭的channel
  3. 类型断言失败,可以通过判断是否断言成功来避免
  4. 数组下标越界
  5. 空指针调用
  6. 除数是0