sync.Map

sync.Map的核心原理就是空间换时间,里面冗余了可读数据和脏数据(这里的冗余指的是指针,所以就算value很大,影响也不大),并且利用了标记删除的方法。dirty提升为read是通过miss来判断的。

load

amended=true

delete

  1. 删除会从read开始,如果read有,就直接删除,删除操作利用atomic.CompareAndSwapPointer把指针替换为nil,因为是原子操作,没有加锁,所以有可能会失败,失败的话就无限循环执行,是有可能会比较消耗性能的。
  2. 如果read没有,并且dirty里面有,就加锁删除,这里也是会用到双检查

store

  1. 先看read中有没有key,如果有就直接修改,修改也是用atomic.CompareAndSwapPointer,然后也是无限循环执行
  2. 如果read中有key并且被标记删除,那就在dirty中加上这个元素,然后在修改这个元素的值
  3. 如果dirty中有,read中没有,就直接修改元素的值就可以
  4. 如果都没有在dirty中加上这个元素并把amended设为true

Golang gc,三色是什么?gc的过程?什么时候触发gc?

gc过程中会把对象分为三种颜色,分别是白色、灰色、黑色

  1. 进程空间中的对象可以看做是一张图,初始状态下所有的对象都是白色的
  2. stop the world,第一轮先扫描所有可达的内存对象
  3. start the world,第二轮将第一轮的对象引用的对象都标记为黑色,然后第一轮的标记为黑色
  4. 持续第二轮的操作,直到遍历完,然后清理白色对象(第二轮的时候实际上gc和应用逻辑是一起跑的,为了保证不误回收,Golang存在写屏障,简单来说就是为了避免在第二轮操作的时候把一个新的引用加在一个黑色对象上,造成一个不可达)

go1.12之后的一个坑点:

触发时间

  1. 定时调用
  2. 内存达到阈值就会引发调用
  3. 手动调用,runtime.gc()

Golang怎么处理并发

协程

缓冲队列

缓冲队列+协程池

Map的内部实现,为什么并发不安全,体现在哪?

数据结构

hmap的buckets是一个bmap数据,bmap的数据结构如下

topbits存放了hash结果的高8位,这样的存放的优势在于减少直接用key,value比较是否相等的次数,可以加速读取过程。这里是一个比较巧妙的设计,选择桶序号是hash结果的最低几位,bmap里面的存放的却是hash结果的最高几位,这样是为了减少同一个桶中有大量相等的tophash,导致读性能下降。(这里的原理很简单,如果hmap分桶的位和bmap查找的位都是前面几位,高位结果重复的概率就会增大)

keys就是存放了key的数组,values就是存放了value的数组,这两个数组的长度都是8。

overflow是指向溢出桶的指针,方便当bmap存满之后去存和查溢出桶。

map的基本原理

  1. 通过hash函数把key计算之后得到一个hash结果
  2. 根据hash的结果得到一个数组的序号
  3. 根据这个数组的序号去访问或者读取
  4. 当然,因为存在不同key的hash结果相同的情况,这时候就叫做哈希碰撞,通常解决哈希碰撞的方法有两种,分别是开放寻址法和拉链法,map的实现是拉链法
  5. 拉链法的原理就是数组的元素都是一个链表,发生哈希碰撞的时候就把元素接着放到链表的末尾(golang map如上所述是用的长度为8的数组,所以需要考虑扩容),访问时候也是一样的道理

map扩容

当map表的负载因子,或者溢出的桶太多的时候就会发生扩容,大概就是把buckets中的数据转移到oldbuckets,因为涉及等量扩容和翻倍扩容,所以对应的分流策略也有些差异,这里就不详细说了



GMP

G:goroutine,go程序建立的协程,主要来保存goroutine运行时的栈信息

M:machine,一个M关联一个操作系统级别的线程。

P:processor,代表了M所需的上下文环境,也是处理用户级代码逻辑的处理器。可以看作一个局部的调度器使go代码在一个线程上跑。

P
MM
GPG
PGMPGP

一个M会连接着一个P,当M堵塞的时候,P就会切换到另外一个M上去,执行本地队列中的goroutine。

当P的本地队列满的时候时候就会把G放入全局队列,为了避免全局队列被饿死,会定时轮询全局队列。

goroutine

轻量级的用户态的线程,go能帮忙调度goroutine的执行,代码编写者就可以像写同步代码一样编写逻辑,在遇到系统调用/IO/sleep之类的操作会把cpu自动出让,给到别的M和G去执行,执行完之后M会再去找P,如果没找到空余的P,就会丢到全局队列中。会有sysmon线程来定时调度任务。

channel

特性

  1. goroutine线程安全
  2. 不同的goroutine之间存储和传输值,提供FIFO语义(也就是先进先出,主要是通过锁来控制的,先拿到的锁就先操作,有个循环队列会保证数据的顺序)
  3. 可以让goroutine堵塞或者非阻塞(非阻塞是怎么实现的?)

channel的主要数据结构是hchan

buf是用来存放数据缓存buf

发送接受过程

  1. 加锁
  2. 把数据从gorountine复制到buf,或者把数据从buf复制到goroutine中,sendx/recvx加1
  3. 释放锁

这样的发送接受模式印证了go的设计模式,通过通信来共享内存

channel满了之后的发送接受

Go的goroutine是用户态的线程,用户态的线程是需要自己去调度的,这个过程go在运行的时候会帮我们调度。

G1:发送数据到channel里的协程

G2:从channel接受数据的协程

当G1执行send的时候,发现缓存满了,会主动调用go的调度器,把M出让。G1会抽象成含有G1指针和send元素的sudog结构体保存在hchan的sendq中,等待被唤醒。G2执行recv操作之后,从缓存队列中取出数据,channel会将等待队列中的G1推出,将G1当时send的数据推到缓存中,然后唤醒G1,放到对应P的执行队列中。

channel没数据,G2堵塞的情况

G2主动出让P,然后让G2等待,把G2打包为sudog放入hchan的recvq中。当G1推送数据时,不会为buf加锁,直接就把数据复制进去。然后hchan就会把G2推出,放到执行的P的队列中。

context

当你代码需要承载比较繁重的业务逻辑的时候,一个协程下会开很多个子协程,子协程下又会有子协程,这时如果和你需要一次全面的终止会比较困难,你可能会用channel,但是使用channel会有一个问题,当你只想结束某个函数以及下面的子协程而不是根函数下面的所有协程的时候就会需要传递很多channel变量,幸运的是go已经通过context帮我们实现了这个功能。

Done,当context被取消或者超时的时候会返回一个close的channel,可以作为广播通知

Err,返回context为什么被取消

Deadline,返回context何时会超时

Value,返回context相关的数据

context.BackGround

返回所有context的root。

context.WithCancel

WitchCancel返回一个继承的Context,这个context在父Context的Done被关闭时关闭自己的Done通道,或者自己被Cancel关闭时关闭自己的Done。

其实如果你有在golang上使用链路追踪的经验,你会发现traces的上下文的信息都是通过context来传递的,因为context的传递链也是一颗多叉树

sync.pool

数组和slice的区别,slice原理和扩容

数组在创建时需要指定长度,slice支持扩容。

21.2521.25

实际上在一个切片的基础上创建一个新的分片,指向的内存空间是一致的,所以在slice作为函数传参的时候需要注意,复制的只是slice的指针,而且append操作如果触发扩容的话会创建一个新的slice。

Go语言内存分配,什么分配在堆上,什么分配在栈上,什么情况会内存逃逸,对应的解决措施是啥?

内存泄漏的情况:

  1. 获取一段长字符串中的一段导致长字符串未释放
  2. 获取一段长slice中的一段,导致长slice未被释放
  3. 在长slice新建slice导致泄漏
  4. goroutine泄漏
  5. time.Ticker未关闭导致泄漏
  6. Finalizer导致泄漏
  7. Deferring Function Call导致泄漏


profile工具

pprofile