一、Go相关

 

参考书籍:

tcmalloc

https://blog.csdn.net/aaronjzhang/article/details/8696212

http://legendtkl.com/2015/12/11/go-memory/

tcmalloc是线程缓存的malloc,实现了高效的多线程内存管理,用于替代系统的内存分配相关的函数

tcmalloc分配的内存主要来自两个地方:全局缓存堆和进程的私有缓存。对于一些小容量的内存申请试用进程的私有缓存,私有缓存不足的时候可以再从全局缓存申请一部分作为私有缓存。对于大容量的内存申请则需要从全局缓存中进行申请。而大小容量的边界就是32k。缓存的组织方式是一个单链表数组,数组的每个元素是一个单链表,链表中的每个元素具有相同的大小

Go实现的内存管理简单的说就是维护一块大的全局内存,每个线程(Go中为Processer)维护一块小的私有内存,私有内存不足的时候再从全局申请。

* go程序启动时会申请一块大内存,并划分为spans/bitmap/arena区域

* arena区域按页划分为一个个小块

* span管理一个或多个页

* mcentral管理多个span供线程申请使用

* mcache作为线程私有资源,资源来源于mcentral

1、GC垃圾回收

垃圾回收的核心就是标记出哪些内存还在使用中(即被引用到),哪些内存不再使用了(即未被引用),把未被引用的内存回收掉,以供后续内存分配时使用。

当前Golang使用的垃圾回收机制是三色标记法配合写屏障和辅助GC,三色标记法是标记-清除法的一种增强版本(减少stw暂停时间)。

三色标记法

三色标记,通过字面意思我们就可以知道它由3种颜色组成:

  1. 黑色 Black:表示对象是可达的,即使用中的对象,黑色是已经被扫描的对象。

  2. 灰色 Gary:表示被黑色对象直接引用的对象,但还没对它进行扫描。

  3. 白色 White:白色是对象的初始颜色,如果扫描完成后,对象依然还是白色的,说明此对象是垃圾对象。

三色标记规则:黑色不能指向白色对象。即黑色可以指向灰色,灰色可以指向白色。

三色标记法是对标记阶段的改进,原理如下:

  1. 初始状态所有对象都是白色。
  2. 从root根出发扫描所有根对象,将他们引用的对象标记为灰色
  3. 分析灰色对象是否引用了其他对象。如果没有引用其它对象则将该灰色对象标记为黑色;如果有引用则将它变为黑色的同时将它引用的对象也变为灰色
  4. 重复步骤3,直到灰色对象队列为空。此时白色对象即为垃圾,进行回收。

(root区域主要是程序运行到当前时刻的栈和全局数据区域。)

如何实现GC和用户代码并行呢?

1.写屏障(Write Barrier)

垃圾回收触发时机

1)内存分配量达到阈值出发GC

每次内存分配时都会检查当前内存分配量是否已达到阈值,如果达到阈值则立即启动GC。

阈值 = 上次GC内存分配量 * 内存增长率
GOGC
2)定期触发GC
src/runtime/proc.go:forcegcperiod
3)手动触发
runtime.GC()

GC性能优化

GC性能与对象数量负相关,对象越多GC性能越差,对程序影响越大。

所以GC性能优化的思路之一就是减少对象分配个数,比如对象复用或使用大对象组合多个小对象等等。

另外,由于内存逃逸现象,有些隐式的内存分配也会产生,也有可能成为GC的负担。

2、逃逸分析

https://zhuanlan.zhihu.com/p/145468000

逃逸分析(Escape analysis)是指由编译器决定内存分配的位置,不需要程序员指定。 

逃逸策略

每当函数中申请新的对象,编译器会跟据该对象是否被函数外部引用来决定是否逃逸:

  1. 如果函数外部没有引用,则优先放到栈中;
  2. 如果函数外部存在引用,则必定放到堆中;

注意,对于函数外部没有引用的对象,也有可能放到堆中,比如内存过大超过栈的存储能力。

逃逸场景:指针逃逸、栈空间不足逃逸、动态类型逃逸、闭包引用对象逃逸

逃逸总结:

  • 栈上分配内存比在堆中分配内存有更高的效率
  • 栈上分配的内存不需要GC处理
  • 堆上分配的内存使用完毕会交给GC处理
  • 逃逸分析目的是决定内分配地址是栈还是堆
  • 逃逸分析在编译阶段完成
go build -gcflags=m

函数传递指针真的比传值效率高吗? 我们知道传递指针可以减少底层值的拷贝,可以提高效率,但是如果拷贝的数据量小,由于指针传递会产生逃逸,可能会使用堆,也可能会增加GC的负担,所以传递指针不一定是高效的。

3、协程调度

对于 进程、线程,都是有内核进行调度,有 CPU 时间片的概念,进行 抢占式调度(有多种调度算法)

对于 协程(用户级线程),这是对内核透明的,也就是系统并不知道有协程的存在,是完全由用户自己的程序进行调度的,因为是由用户程序自己控制,那么就很难像抢占式调度那样做到强制的 CPU 控制权切换到其他进程/线程,通常只能进行 协作式调度,需要协程自己主动把控制权转让出去之后,其他协程才能被执行到。

Golang 运行调度。在 Golang 里面有三个基本的概念:G, M, P。

  • G: Goroutine 执行的上下文环境。
  • M: 操作系统线程。
  • P: Processer。进程调度的关键,调度器,也可以认为约等于 CPU。

M必须拥有P才可以执行G中的代码,P含有一个包含多个G的队列,P可以调度G交由M执行。

P的个数在程序启动时决定,默认情况下等同于CPU的核数,由于M必须持有一个P才可以运行Go代码,所以同时运行的M个数,也即线程数一般等同于CPU的个数,以达到尽可能的使用CPU而又不至于产生过多的线程切换开销。

runtime.GOMAXPROCS()

每个P维护着一个包含G的队列,不考虑G进入系统调用或IO操作的情况下,P周期性的将G调度到M中执行,执行一小段时间,将上下文保存下来,然后将G放到队列尾部,然后从队列中重新取出一个G进行调度。

除了每个P维护的G队列以外,还有一个全局的队列,每个P会周期性的查看全局队列中是否有G待运行并将期调度到M中执行,全局队列中G的来源,主要有从系统调用中恢复的G。之所以P会周期性的查看全局队列,也是为了防止全局队列中的G被饿死。

2)系统调用

一般情况下M的个数会略大于P的个数,这多出来的M将会在G产生系统调用时发挥作用。

当G0即将进入系统调用时,M0将释放P,进而某个空闲的M1获取P,继续执行P队列中剩下的G。而M0由于陷入系统调用而进被阻塞,M1接替M0的工作,只要P不空闲,就可以保证充分利用CPU。

M1的来源有可能是M的缓存池,也可能是新建的。当G0系统调用结束后,跟据M0是否能获取到P,将会将G0做不同的处理:

  1. 如果有空闲的P,则获取一个P,继续执行G0。
  2. 如果没有空闲的P,则将G0放入全局队列,等待被其他的P调度。然后M0将进入缓存池睡眠。

3)工作量窃取

多个P中维护的G队列有可能是不均衡的,P已经将G全部执行完,然后去查询全局队列,全局队列中也没有G,而另一个M中除了正在运行的G外,队列中还有3个G待运行。此时,空闲的P会将其他P中的G偷取一部分过来,一般每次偷取一半。

GOMAXPROCS设置对性能的影响

一般来讲,程序运行时就将GOMAXPROCS大小设置为CPU核数,可让Go程序充分利用CPU。 在某些IO密集型的应用里,这个值可能并不意味着性能最好。 理论上当某个Goroutine进入系统调用时,会有一个新的M被启用或创建,继续占满CPU。 但由于Go调度器检测到M被阻塞是有一定延迟的,也即旧的M被阻塞和新的M得到运行之间是有一定间隔的,所以在IO密集型应用中不妨把GOMAXPROCS设置的大一些,或许会有好的效果。

 协程泄露

有channel一直没结束,会一直占用cpu。可以通过设置time out以及select会结束channel。

4、并发编程

4.1 Channel

channel是一个通信机制,它可以让一个goroutine通过它给另一个goroutine发送值信息。也可用于并发控制。

使用channel来控制子协程的优点是实现简单,缺点是当需要大量创建协程时就需要有相同数量的channel,而且对于子协程继续派生出来的协程不方便控制。

channel有哪些状态

channel有三种状态:

  1. nil,未初始化的状态,只进行了声明,或者手动赋值为nil
  2. active,正常的channel,可读可写
  3. closed,已关闭

channel可进行三种操作:

  1. 关闭

这三种操作和状态可以组合出九种情况:

操作nil的channel正常channel已关闭channel
<-ch (读) 阻塞 成功或阻塞 读到零值
ch<- (写) 阻塞 成功或阻塞 panic
close(ch) (关闭) panic 成功 panic

 无缓冲的 channel 和有缓冲的 channel 的区别?

非缓冲 channel,channel 发送和接收动作是同时发生的
例如 ch := make(chan int) ,如果没 goroutine 读取接收者<-ch ,那么发送者ch<- 就会一直阻塞
缓冲 channel 类似一个队列,只有队列满了才可能发送阻塞

 

如何优雅的退出协程?

for-rangerange,okfor-selectstopCh

CSP 模型由并发执行的实体(线程或者进程)所组成,实体之间通过发送消息进行通信,这里发送消息时使用的就是通道,或者叫 channel。

WaitGroup,可理解为Wait-Goroutine-Group,即等待一组goroutine结束。

内部实现使用了信号量:

信号量是Unix系统提供的一种保护共享资源的机制,用于防止多个线程同时访问某个资源。

可简单理解为信号量为一个数值:

  • 当信号量>0时,表示资源可用,获取信号量时系统自动将信号量减1;
  • 当信号量==0时,表示资源暂不可用,获取信号量时,当前线程会进入睡眠,当信号量为正时被唤醒;

WaitGroup实现中也使用了信号量。

WaitGroup对外提供三个接口:

  • Add(delta int): 将delta值加到counter中
  • Wait(): waiter递增1,并阻塞等待信号量semaphore
  • Done(): counter递减1,按照waiter数值释放相应次数信号量

整体处理过程:

  1. 启动goroutine前将计数器通过Add(2)将计数器设置为待启动的goroutine个数。
  2. 启动goroutine后,使用Wait()方法阻塞自己,等待计数器变为0。
  3. 每个goroutine执行结束通过Done()方法将计数器减1。
  4. 计数器变为0后,阻塞的goroutine被唤醒。

它与WaitGroup最大的不同点是context对于派生goroutine有更强的控制力,它可以控制多级的goroutine。

context翻译成中文是"上下文",即它可以控制一组呈树状结构的goroutine,每个goroutine拥有相同的上下文。

 

context包中定义了一个空的context, 名为emptyCtx,用于context的根节点,空的context只是简单的实现了Context,本身不包含任何值,仅用于其他context的父节点。context包中定义了一个公用的emptCtx全局变量,名为background,可以使用context.Background()获取它。

* Context仅仅是一个接口定义,跟据实现的不同,可以衍生出不同的context类型;

* cancelCtx实现了Context接口,通过WithCancel()创建cancelCtx实例;

* timerCtx实现了Context接口,通过WithDeadline()和WithTimeout()创建timerCtx实例;

* valueCtx实现了Context接口,通过WithValue()创建valueCtx实例;

* 三种context实例可互为父节点,从而可以组合成不同的应用形式;

 5、函数、方法、Interface与反射

函数:Func + 函数名 + 参数 + 返回值(可选) + 函数体

方法:一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的接收者可以是值接收者,也可以是指针接收者。所有给定类型的方法属于该类型的方法集。Go语言不允许为简单的内置类型添加方法。

如果实现了接收者是值类型的方法,会隐含地也实现了接收者是指针类型的方法。

如果方法的接收者是值类型,无论调用者是对象还是对象指针,修改的都是对象的副本,不影响调用者;如果方法的接收者是指针类型,则调用者修改的是指针指向的对象本身。

使用指针作为方法的接收者的理由:

* 方法能够修改接收者指向的值。
* 避免在每次调用方法时复制该值,在值的类型为大型结构体时,这样做会更加高效。


Interface:

interface是一组method签名的组合,我们通过interface来定义对象的一组行为。interface类型定义了一组方法,如果某个对象实现了某个接口的所有方法,则此对象就实现了此接口。

通过类型断言可以判断interface的类型或者switch语句判断。

 

首先需要把它转化成reflect对象(reflect.Type或者reflect.Value,根据不同的情况调用不同的函数)。这两种获取方式如下:

1)struct

继承、封装、多态

继承:一个对象获得另一个对象的属性的过程

go可以实现多继承

  • 一个struct嵌套了另一个匿名struct,那么这个struct可以直接访问匿名机构提的方法,从而实现集成
  • 一个struct嵌套了另一个命名的struct,那么这个模式叫做组合
  • 一个struct嵌套了多个匿名struct,那么这个结构可以直接访问多个匿名struct的方法,从而实现多重继承

封装:自包含的黑盒子,有私有和公有的部分,公有可以被访问,私有的外部不能访问

  • go通过约定来实现权限控制。变量名首字母大写,相当于public,首字母小写,相当于private。在同一个包中访问,相当于default。由于在go中没有继承,所以就没有protected

多态:允许用一个接口在访问同一类动作的特性

合约

struct比较:

实例不能比较,指针可以比较,能否比较看字段是否有不可比较的数据类型。

  • 可排序的数据类型有三种,Integer,Floating-point,和String
  • 可比较的数据类型除了上述三种外,还有Boolean,Complex,Pointer,Channel,Interface和Array
  • 不可比较的数据类型包括,Slice, Map, 和Function

2) 数组和slice

array特点:

  • go的数组是值类型,也就是说一个数组赋值给另一个数组,那么实际上就是真个数组拷贝了一份,需要申请额外的内存空间
  • 如果go中的数组做为函数的参数,那么实际传递的参数是一份数组的拷贝,而不是数组的指针
  • array的长度也是Type的一部分,这样就说明[10]int和[20]int是不一样的

slice特点:

  • slice是一个引用类型,是一个动态的指向数组切片的指针
  • slice是一个不定长的,总是指向底层的数组array的数据结构

区别:

  • 声明时:array需要声明长度或者…
  • 做为函数参数时:array传递的是数组的副本,slice传递的是指针

3)map

map底层通过哈希表实现。

go的map并发访问是不安全的,会出现未定义行为,导致程序退出。

go1.6之前,内置的map类型是部分goroutine安全的,并发的读没有问题,并发的写可能有问题。go1.6之后,并发的读写map会报错。

sync.Map。
sync.Map
  1. 空间换时间。通过冗余的两个数据结构(read,dirty),实现加锁对性能的影响
  2. 使用只读数据(read),避免读写冲突
  3. 动态调整,miss次数多了之后,将dirty数据提升为read
  4. double-checking
  5. 延迟删除。删除一个键值只是打标记,只有在提升dirty的时候才清理删除的数据
  6. 优先从read读取、更新、删除,因为对read的读取不需要锁

delete删除元素,只是将map的key对应的元素置为empty。并没有删除内存中的数据。

7、关键字

  • defer定义的延迟函数参数在defer语句出时就已经确定下来了
  • defer定义顺序与实际执行顺序相反
  • return不是原子操作,执行过程是: 保存返回值(若有)-->执行defer(若有)-->执行ret跳转
  • 申请资源后立即使用defer关闭资源是好习惯
  • select语句中除default外,每个case操作一个channel,要么读要么写
  • select语句中除default外,各case执行顺序是随机的
  • select语句中如果没有default语句,则会阻塞等待任一case
  • select语句中读操作要判断是否成功读取,关闭的channel也可以读取

3)range

range是Golang提供的一种迭代遍历手段,可操作的类型有数组、切片、Map、channel等,

4)mutex/rwmutex

8、其他知识点 

new和make的区别

  • new和make都在堆上分配内存,但是它们的行为不同,适用于不同的类型。

go方法传参比起python、java有什么区别

  • go中的函数的参数传递采用的是值传递 

go init方法

大家都知道golang里的main函数是程序的入口函数,main函数返回后,程序也就结束了。golang还有另外一个特殊的函数init函数,先于main函数执行,实现包级别的一些初始化操作,今天我们就深入介绍下init的特性。

init函数的主要作用:

  • 初始化不能采用初始化表达式初始化的变量。
  • 程序运行前的注册。
  • 实现sync.Once功能。
  • 其他

init函数的主要特点:

  • init函数先于main函数自动执行,不能被其他函数调用;
  • init函数没有输入参数、返回值;
  • 每个包可以有多个init函数;
  • 包的每个源文件也可以有多个init函数,这点比较特殊;
  • 同一个包的init执行顺序,golang没有明确定义,编程时要注意程序不要依赖这个执行顺序。
  • 不同包的init函数按照包导入的依赖关系决定执行顺序。

golang程序初始化

golang程序初始化先于main函数执行,由runtime进行初始化,初始化顺序如下:

  1. 初始化导入的包(包的初始化顺序并不是按导入顺序(“从上到下”)执行的,runtime需要解析包依赖关系,没有依赖的包最先初始化,与变量初始化依赖关系类似,参见golang变量的初始化);
  2. 初始化包作用域的变量(该作用域的变量的初始化也并非按照“从上到下、从左到右”的顺序,runtime解析变量依赖关系,没有依赖的变量最先初始化,参见golang变量的初始化);
  3. 执行包的init函数;

 

PProf

想要进行性能优化,首先瞩目在 Go 自身提供的工具链来作为分析依据,本文将带你学习、使用 Go 后花园,涉及如下:

  • runtime/pprof:采集程序(非 Server)的运行数据进行分析
  • net/http/pprof:采集 HTTP Server 的运行时数据进行分析

是什么

pprof 是用于可视化和分析性能分析数据的工具

pprof 以 profile.proto 读取分析样本的集合,并生成报告以可视化并帮助分析数据(支持文本和图形报告)

profile.proto 是一个 Protocol Buffer v3 的描述文件,它描述了一组 callstack 和 symbolization 信息, 作用是表示统计分析的一组采样的调用栈,是很常见的 stacktrace 配置文件格式

支持什么使用模式

  • Report generation:报告生成
  • Interactive terminal use:交互式终端使用
  • Web interface:Web 界面

可以做什么

  • CPU Profiling:CPU 分析,按照一定的频率采集所监听的应用程序 CPU(含寄存器)的使用情况,可确定应用程序在主动消耗 CPU 周期时花费时间的位置
  • Memory Profiling:内存分析,在应用程序进行堆分配时记录堆栈跟踪,用于监视当前和历史内存使用情况,以及检查内存泄漏
  • Block Profiling:阻塞分析,记录 goroutine 阻塞等待同步(包括定时器通道)的位置
  • Mutex Profiling:互斥锁分析,报告互斥锁的竞争情况

 

二、操作系统

 

- 系统调用

系统调用:用户在编程时可以调用的操作系统功能

系统调用是操作系统提供给编程人员的唯一接口,使CPU状态从用户态陷入内核态