• 博客园logo
  • 会员
  • 周边
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录

sm1le

  • 博客园
  • 联系
  • 订阅
  • 管理

公告

View Post

Golang

1.Slice扩容原理:
(1)1.17版本:
当期望容量 > 两倍的旧容量时,直接使用期望容量作为新切片的容量;
如果旧容量 < 1024(注意这里单位是元素个数),那么直接翻倍旧容量;
如果旧容量 > 1024,那么会进入一个循环,每次增加25%直到大于期望容量
(1)1.18版本:不再与1024比较,而是与256比较,每次扩容也不是1.25倍

2.方法和函数的区别:
函数不属于任何类型或结构体,方法是属于自定义类型或结构体的,方法有接收者,函数没有接收者

3.go参数传递方式
golang中所有参数都是值传递,参数如果是非引用类型,则修改不会影响原数值;参数如果是引用类型(如slice,map,channel),则修改会改变原数值。
slice传参时,只复制len和cap,底层共用一个数组。

4.map相关
map底层主要由两个结构体实现,一个是hmap,一个是bmap,也称bucket,每个桶最多存放8个元素。当B>4时,则认为需要增加2^(B-4)个溢出桶

map查找过程:
通过key计算hash值,低B位为桶的位置,高8位通过tophash来判断key是否在当前桶,若不在则去溢出桶寻找,溢出桶也没有则返回零值,找到key值后,计算出value的位置,因为key和value分开存放

map负载因子
负载因子指每个bucket桶存储的平均元素个数。
负载因子 = 哈希表存储的元素个数/桶个数=6.5

map扩容原理
扩容条件:
(1)超过负载:即存储的元素>负载因子x桶数
(2)溢出桶太多:
当桶总数 < 2 ^ 15 时,如果溢出桶总数 >= 桶总数,则认为溢出桶过多。
当桶总数 >= 2 ^ 15 时,直接与 2 ^ 15 比较,当溢出桶总数 >= 2 ^ 15 时,即认为溢出桶太多了。

扩容机制:
1.双倍扩容:针对条件1,新建一个buckets数组,新的buckets大小是原来的2倍,然后旧buckets数据搬迁到新的buckets,每次最多搬迁2个bucket,即渐进式扩容。
2.等量扩容:针对条件2,并不扩大容量,buckets数量维持不变,重新做一遍类似双倍扩容的搬迁动作,把松散的键值对重新排列一次,使得同一个 bucket 中的 key 排列地更紧密,节省空间,提高 bucket 利用率,进而保证更快的存取

5.GC过程
Golang中垃圾回收采用三色标记法加上混合写屏障辅助完成,过程如下:
三色标记法:
1.初始时所有对象都是白色
2.从root出发,扫描所有可达对象标记为灰色,并加入标记队列
3.从标记队列中循环取出,若有引用对象则将引用对象标记为灰色并加入队列,直到队列为空
4.此时将白色对象即为垃圾对象

三色不变性
强三色不变性:所有的黑色对象不会指向白色对象,只会指向黑色或灰色对象
弱三色不变性:黑色对象可以指向白色对象,但必须有灰色对象到达该白色对象的路径

屏障技术
插入写屏障:当一个对象引用另外一个对象时,将另外一个对象标记为灰色,以此满足强三色不变性,不会存在黑色对象引用白色对象。(堆内生效,栈内不生效,所以需要对栈上重新进行一次stw,然后再rescan一次)
删除写屏障:在灰色对象删除对白色对象的引用时,将白色对象置为灰色,其实就是快照保存旧的引用关系,这叫STAB(snapshot-at-the-beginning),以此满足弱三色不变性。
混合写屏障:GC刚开始的时候,会将栈上的可达对象全部标记为黑色;GC期间,任何在栈上新创建的对象,均为黑色。可以不用rescan所有协程栈对象。

GC过程
1.短暂stw,统计root中的任务数量,开启写屏障,初始所有对象都是白色
2.start the world,扫描root的所有对象,加入标记队列变为灰色,循环处理标记队列直到为空,将出队的对象变为黑色,如果过程中触发写屏障,则将对象变为灰色并加入单独的扫描队列中,所有新建的对象直接标记为黑色
3.stop the world,将写屏障记录的对象放入标记队列进行循环处理
4.start the world,清扫所有白色的对象,关闭写屏障,新建对象标记为白色
Note:root:全局变量、执行栈(包括栈上对象以及指向堆的指针)、寄存器

6.简述GMP
GMP模型:
processor列表:所有P在程序运行时确定,数量由GOMAXPROCS确定,每个P拥有自己的本地队列
P本地队列:新建的goroutine会优先存放在本地队列中,若队列满了,则存放在全局队列中
全局队列:存放等待运行的G
线程M:每个M绑定一个P,M会运行本地队列中的G,若本地队列为空,则去全局队列中获取G,若全局队列也为空,则去其他P的本地队列中偷取G来运行

调度策略:
work-stealing:当本线程无可运行的 G 时,会尝试从其他线程绑定的 P 偷取 G,而不是把线程销毁。
hand-off:当本线程因为 G 进行系统调用阻塞时,线程释放绑定的 P,把 P 转移给其他空闲的线程执行。
为防止其他goroutine被饿死,一个goroutine最多占用CPU 10ms(抢占)

GMP过程:
1.程序运行时创建所有的P本地队列,创建goroutine会优先入本地队列,若本地已满则进入全局队列等待执行
2.M和P绑定并执行本地队列中的G,若本地为空,则去全局获取,否则去偷取别的P队列获取G来执行
3.当M执行G时发生系统调用出现阻塞,M会接触和当前P的绑定,P会新建或者从休眠M队列中获取新的M来运行G
4.当系统调用完成后,这个M会尝试获取空闲的P,并将G放入本地队列继续执行,如果没有M则进入休眠状态加入休眠队列,G放入全局队列等待执行

扩展:
1.加入P的原因:
一开始的GM模型是没有P的,每个M直接与全局队列交互,此时会导致严重的锁竞争问题,并且发生系统调用会频繁阻塞/唤醒M,造成很大的开销。
引入P之后,P拥有自己的本地队列,大大减少了M对全局队列的依赖,因此大幅度降低了锁竞争。
2.为什么不直接在M上扩展:
一般来说,M的数量远远大于P的数量,当发生阻塞时,需要创建新的M,如果本地队列绑定在M上,则本地队列会随着M增加不断增加,降低性能。
并且当M发生阻塞时,不希望其他等待的任务因此随之阻塞,因此引入P能很好的解决该问题,P可以获取空闲的M继续执行G

7.Mutex相关
两种模式:
1.正常模式(非公平锁):正常模式下,一个尝试加锁的goroutine会自旋几次,若仍得不到锁,则进入队列顺序等待。等待队列中唤醒的goroutine并不会立刻获得锁,而是要和在自旋的goroutine竞争,但因为自旋的G在CPU上运行,更容易获得锁,则唤醒的G将回到等待队列的头部继续等待。
2.饥饿模式(公平锁):当一个G等待时间超过1ms时,将会进入饥饿模式。饥饿模式下,unlock的锁会直接传给队列头部的G,新来的G不会自旋,而是直接进入队列尾部等待。当获得锁的G等待时间小于1ms或者等待队列为空时,则从饥饿模式切换到正常模式。

自旋条件:
1.锁被占用,且处于正常模式下
2.累计自旋次数小于最大自旋次数
3.CPU核数不为1
4.有空闲的P,且P队列为空

8.Context包相关
用途:用来处理多个goroutine之间的数据、取消操作、截止时间等操作。

函数:

type CancelFunc func()
Background() Context:返回一个空的context,一般作为树的根节点
func WithCancel(parent Context) (ctx Context, cacl CancelFunc):
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context

注意:
1.不要把 Context 放在结构体中,要以参数的方式传递
2.以 Context 作为参数的函数方法,应该把 Context 作为第一个参数,放在第一位
3.给一个函数方法传递 Context 的时候,不要传递 nil,如果不知道传递什么,就使用 context.TODO
4.Context 的 Value 相关方法应该传递必须的数据,不要什么数据都使用这个传递
5.Context 是线程安全的,可以放心的在多个 goroutine 中传递

9.内存管理

channel底层原理

type hchan struct {
  //channel分为无缓冲和有缓冲两种。
  //对于有缓冲的channel存储数据,借助的是如下循环数组的结构
	qcount   uint           // 循环数组中的元素数量
	dataqsiz uint           // 循环数组的长度
	buf      unsafe.Pointer // 指向底层循环数组的指针
	elemsize uint16 //能够收发元素的大小
	closed   uint32   //channel是否关闭的标志
	elemtype *_type //channel中的元素类型
  //有缓冲channel内的缓冲数组会被作为一个“环型”来使用。
  //当下标超过数组容量后会回到第一个位置,所以需要有两个字段记录当前读和写的下标位置
	sendx    uint   // 下一次发送数据的下标位置
	recvx    uint   // 下一次读取数据的下标位置
  //当循环数组中没有数据时,收到了接收请求,那么接收数据的变量地址将会写入读等待队列
  //当循环数组中数据已满时,收到了发送请求,那么发送数据的变量地址将写入写等待队列
	recvq    waitq  // 读等待队列
	sendq    waitq  // 写等待队列
	lock mutex //互斥锁,保证读写channel时不存在并发竞争问题
}

当一个goroutine向缓冲区满的channel写数据时,调度器检测到缓冲区已满,将G状态置为waiting,并移除与M的联系,进入写等待队列中,进入阻塞状态;当其他G从channel中取数据时,调度器会唤醒G,G进入P队列等待执行。
当一个goroutine从空的channel中取数据时,也会出现阻塞。

defer、return、panic执行顺序

1.defer和return:首先给返回值赋值,之后执行defer语句,最后执行return携带返回值退出函数。
2.defer和panic:执行到panic程序停止,之后执行defer语句,若recovery,会执行主协程剩下语句,但panic后语句无法执行。

内存逃逸

1.内存逃逸指原先应该分配在栈上的变量逃逸到了堆上
2.原因:
动态类型逃逸:编译期间难以确定的参数类型
函数返回局部指针、栈空间不足、闭包引用等

posted on 2022-07-26 20:24  sm1le  阅读(247)  评论(0)    收藏  举报

刷新页面返回顶部
 
博客园  ©  2004-2026
浙公网安备 33010602011771号 浙ICP备2021040463号-3