golang的语言类型
golang是强类型的编译型语言
强类型定义语言:
强制数据类型定义的语言。也就是说,一旦一个变量被指定了某个数据类型,如果不经过强制转换,那么它就永远是这个数据类型了。举个例子:如果你定义了一个整型变量a,那么程序根本不可能将a当作字符串类型处理。强类型定义语言是类型安全的语言。
弱类型定义语言:
数据类型可以被忽略的语言。它与强类型定义语言相反, 一个变量可以赋不同数据类型的值。
解释型语言
使用专门的解释器对源程序逐行解释成特定平台的机器码并立即执行。是代码在执行时才被解释器一行行动态翻译和执行,而不是在执行之前就完成翻译。
编译型语言
使用专门的编译器,针对特定的平台,将高级语言源代码一次性的编译成可被该平台硬件执行的机器码,并包装成该平台所能识别的可执行性程序的格式。只需编译一次,以后运行时不需要编译,所以编译型语言执行效率高。
slice和数组的区别
(1)go是有数组的,只是平时用切片比较多。数组大小一旦创建就不能改变,数组长度大于元素个数的时候会用0补位,这跟其他语言是相通的。
(2)切片slice可以看作是对数组的一切操作,它是一个引用数据类型,其数据结构包括底层数组的地址,以及元素可操作长度len或可扩容长度cap。
(3)要想突破slice的扩容cap限制进行无限扩容就需要使用append()函数进行操作。如果append追加的元素后slice的总长度不超过底层数组的总长度,那么slice引用的地址不会发生改变,反之引用地址会 变成新的数组的地址。
(4)slice是一个抽象的概念,它存在的意义在于方便对一个顺序结构进行一些方便操作,例如查找,排序,追加等等
.首先看看slice的源码结构:
type slice struct {
array unsafe.Pointer
len int
cap int
}
slice是一个特殊的引用类型,但是它自身也是个结构体
属性len表示可用元素数量,读写操作不能超过这个限制,不然就会panic
属性cap表示最大扩张容量,当然这个扩张容量也不是无限的扩张,它是受到了底层数组array的长度限制,超出了底层array的长度就会panic
Go 中切片扩容的策略是这样的:
首先判断,如果新申请容量大于2倍的旧容量,最终容量就是新申请的容量
在 1.18 版本前,切片扩容,在容量小于1024时,以2倍大小扩容。超过1024后,以1.25倍扩容。
在扩容后切片的基础上,会根据长度和容量进行 roundupsize 。
在1.18版本后,切片扩容,在容量小于256时,以2倍大小扩容。超过256后,以(1.25倍+192)扩容。
map
go map 实现
Go语言中的map是也基于哈希表实现的,它解决哈希冲突的方式是链地址法,选择桶采用的是 与运算,通过使用数组+链表的数据结构来表达map。
哈希冲突
当两个不同的key落在了同一个桶中,这时就发生了哈希冲突。go的解决方式是链地址法:在桶中按照顺序寻到第一个空位,若有位置,则将其置于其中;否则,判断是否存在溢出桶,若有溢出桶,则去该桶的溢出桶中寻找空位,如果没有溢出桶,则添加溢出桶,并将其置溢出桶的第一个空位
Go语言的map,底层是哈希表实现的,通过链地址法解决哈希冲突,它依赖的核心数据结构是数组加链表。
map 数据结构
type hmap struct {
count int // 代表哈希表中的元素个数,调用len(map)时,返回的就是该字段值。
flags uint8 // 状态标志,下文常量中会解释四种状态位含义。
B uint8 // buckets(桶)的对数log_2(哈希表元素数量最大可达到装载因子*2^B)
noverflow uint16 // 溢出桶的大概数量。
hash0 uint32 // 哈希种子。
buckets unsafe.Pointer // 指向buckets数组的指针,数组大小为2^B,如果元素个数为0,它为nil。
oldbuckets unsafe.Pointer // 如果发生扩容,oldbuckets是指向老的buckets数组的指针,老的buckets数组大小是新的buckets的1/2。非扩容状态下,它为nil。
nevacuate uintptr // 表示扩容进度,小于此地址的buckets代表已搬迁完成。
extra *mapextra // 这个字段是为了优化GC扫描而设计的。当key和value均不包含指针,并且都可以inline时使用。extra是指向mapextra类型的指针。
创建map
map初始化有以下两种方式
make(map[k]v)
// 指定初始化map大小为hint
make(map[k]v, hint)
对于不指定初始化大小,和初始化值hint<=8(bucketCnt)时go会调用makemap_small函数并直接从堆上进行分配。
当hint>8时,则调用makemap函数
map扩容
当向桶中添加了很多 key,造成元素过多,超过了装载因子所设定的程度(即元素个数 >= 桶(bucket)总数 * 6.5)
,或者多次增删操作,造成溢出桶过多,均会触发扩容。
增量扩容:会增加桶的个数(增加一倍),把原来一个桶中的 keys 被重新分配到两个桶中。
等量扩容:(使用了较多的溢出桶,但没有超过负载因子,有些元素会删除掉,则重新排列)
不会更改桶的个数,只是会将桶中的数据变得紧凑。不管是增量扩容还是等量扩容,都需要创建新的桶数组,并不是原地操作的。
等量扩容的目的就是为了把松散的键值对重新排列一次。使用更少的溢出桶。
map 是并发安全的吗 为啥
1.不是 :golang map 没有做并发控制 ,加锁或使用其他同步机制会增加复杂性和开销
如何实现并发安全的 map
1.加锁 : 对整个map加上读写锁sync.RWMutex
2.sync.map
sync.Map思想:就是用两个数据结构(只读的 read 和可写的 dirty)尽量将读写操作分开,来减少锁对性能的影响
golang new 和 make
new“这是一个用来分配内存的内建函数它并不初始化内存,只是将其置零。也就是说,**new(T)会为T类型的新项目,分配被置零的存储,并且返回它的地址,一个类型为*T的值(指针类型)。**在Go的术语中,其返回一个指向新分配的类型为T的指针,这个指针指向的内容的值为零(zero value)。注意并不是指针为零。
make: make用来初始化特定的数据结构。它只用来创建slice,map和channel,并且返回一个初始化的(而不是置零),类型为T的值
Golang的参数传递、引用类型
Go语言中所有的传参都是值传递(传值),都是一个副本,一个拷贝。因为拷贝的内容有时候是非引用类型(int、string、struct等这些),这样就在函数中就无法修改原内容数据;有的是引用类型(指针、map、slice、chan等这些),这样就可以修改原内容数据。
Golang的引用类型包括 slice、map 和 channel。它们有复杂的内部结构,除了申请内存外,还需要初始化相关属性。内置函数 new 计算类型大小,为其分配零值内存,返回指针。而 make 会被编译器翻译成具体的创建函数,由其分配内存和初始化成员结构,返回对象而非指针。
Golang导入包时,为什么可能使用’_’/’.'导入?
包前是下划线_
当导入一个包时,该包下的文件里所有init函数都会被执行,但是有时我们仅仅需要使用init函数而已并不希望把整个包导入(不使用包里的其他函数)
包前是点 .
import(.“fmt”)
这个点操作的含义就是这个包导入之后在你调用这个包的函数时,你可以省略前缀的包名,也就是前面你调用的fmt.Println(“hello world”)可以省略的写成Println(“hello world”)
Golang main方法和init方法
- 两者皆为golang保留的方法,皆无参数的返回值
- main函数是程序的入口,整个项目只能有一个。
- init函数在每个package是可选的,可有可无,可有多个
- init函数先于main函数自动执行,不能被其他函数调用;
- init函数在导入该package时程序会自动调用init函数
- init方法的执行顺序:按照导入的顺序执行。
- main函数中可以使用flag包来获取和解析命令行参数
golang 的锁
golang中的锁分为两种:互斥锁sync.Mutex 和读写锁sync.RWMutex
互斥锁的实现基于操作系统的原语,如Linux中的pthread_mutex_t
互斥锁:sync.Mutex
1.在Go中,sync.Mutex 提供了互斥锁的实现
2.互斥锁用来控制并发程序对临界资源的访问。
3.提供 Lock() 和 Unlock() 两种操作对应上锁和解锁
4.当执行了 mutex.Lock() 操作后,如果有另外一个 goroutine 又执行了上锁操作,那么该操作被被阻塞,直到该互斥锁恢复到解锁状态。
读写锁:sync.RWMutex
1.读写锁是对读写操作进行加锁。多个读操作之间不存在互斥关系
2.读写锁包含 Lock() Unlock() Rlock() Rnloc() 等操作
3.其中 Lock() 即“写锁”,调用了“写锁”后,不能有其他goroutine进行读或者写操作。 Unlock() 即“写解锁”,调用了“写解锁”后会唤醒所有因为要进行“读锁定(即:RLock())” 而被阻塞的 goroutine。
4.RLock()为“读锁”,调用“读锁”后,不能有其他goroutine进行写操作,但是可以进行读操作。RUnlock() 为“读解锁”,调用“读解锁”后,会唤醒一个因为要进行“写锁定”而被阻塞的goroutine。
defer关键字
defer、 return、返回值 三者的执行顺序是
return 最先给返回值赋值;
接着 defer 开始执行一些收尾工作;
最后 RET 指令携带返回值退出函数。
向 defer 关键字传入的函数会在函数返回之前运行。假设我们在 for 循环中多次调用 defer 关键字,会倒序执行所有向 defer 关键字中传入的表达式。
1)延迟函数的参数在defer语句出现时就已经确定下来了
2)延迟函数执行按后进先出顺序执行,即先出现的defer后执行
4)函数返回过程
有一个事实必须要了解,关键字return不是一个原子操作,实际上return只代理汇编指令ret,即将跳转程序执行。return i,实际上分两步进行,即将i值存入栈中作为返回值,然后执行跳转,而defer的执行时机正是跳转前,所以说defer执行时还是有机会操作返回值的。
一个主函数拥有一**个匿名的返回值,返回使用本地或局部变量,这种情况下,defer语句可以引用到返回值,但不会改变返回值。
主函数声明语句中带名字的返回值,**会被初始化成一个局部变量,函数内部可以像使用局部变量一样使用该返回值。如果defer语句操作该返回值,可能会改变返回结果。
defer修改返回值不会影响到匿名的返回值
具名返回值 会被修改
defer 关键字的实现主要依靠编译器和运行时的协作:
编译期;
将 defer 关键字被转换 runtime.deferproc;
在调用 defer 关键字的函数返回之前插入 runtime.deferreturn;
运行时:
runtime.deferproc 会将一个新的 runtime._defer 结构体追加到当前 Goroutine 的链表头;
runtime.deferreturn 会从 Goroutine 的链表中取出 runtime._defer 结构并依次执行;
在go语言中,Printf()、Sprintf()、Fprintf()函数的区别用法是什么
?
都是把格式好的字符串输出,只是输出的目标不一样:
Printf(),是把格式字符串输出到标准输出(一般是屏幕,可以重定向)。
Printf() 是和标准输出文件(stdout)关联的,Fprintf 则没有这个限制.
Sprintf(),是把格式字符串输出到指定字符串中,所以参数比printf多一个char*。那就是目标字符串地址。
Fprintf(), 是把格式字符串输出到指定文件设备中,所以参数笔printf多一个文件指针FILE*。主要用于文件操作。Fprintf()是格式化输出到一个stream,通常是到文件。
select 和 switch的区别
select与switch的区别:
1:每个switch后面必须跟随一个条件判断,而select后面没有
2:switch中的case语句为枚举值进行比较,select中的case必须是一个对channel的读或者写的操作
select与switch的相同点:
如果switch或select中的case都不成功,那么都会进入default
select只能应用于channel的操作,既可以用于channel的数据接收,也可以用于channel的数据发送。
如果select的多个分支都满足条件,则会随机选取其中一个满足条件的分支,
go 相关命令
go env: #用于查看go的环境变量
go run: #用于编译并运行go源码文件
go build: #用于编译源码文件、代码包、依赖包
go get: #用于动态获取远程代码包
go install: #用于编译go文件,并将编译结构安装到bin、pkg目录
go clean: #用于清理工作目录,删除编译和安装遗留的目标文件
go version: #用于查看go的版本信息
golang 协程
区分 进程,线程,协程
进程是计算机资源分配的最小单位,进程是对处理器资源,虚拟内存的抽象,而虚拟内存是对主存资源和文件(2)的抽象,文件是对I/O设备的抽象。
进程间通信: 信号,信号量,管道,消息队列,共享内存,scoket
线程是计算机调度的最小单位,共享同个进程分配的计算机资源。
线程间通信: 互斥锁,读写锁,条件变量,信号量
协程
golang协程 – goroutine
在Go语言中,每一个并发的执行单元叫作一个goroutine。
Go 协程可以看作是轻量级线程。与线程相比,创建一个 Go 协程的成本很小。因此在 Go 应用中,常常会看到有数以千计的 Go 协程并发地运行。
每个 goroutine (协程) 默认占用内存远比 Java 、C 的线程少。
Go 协程会复用(Multiplex)数量更少的 OS 线程。即使程序有数以千计的 Go 协程,也可能只有一个线程。
goroutine:2KB
线程:8MB
当一个程序启动时,其主函数即在一个单独的goroutine中运行,我们叫它main goroutine。新的goroutine会用go语句来创建。在语法上,go语句是一个普通的函数或方法调用前加上关键字go。go语句会使其语句中的函数在一个新创建的goroutine中运行。而go语句本身会迅速地完成。
Golang 并发控制(channel、context、waitGroup)
channel
channel 是 Go 语言在语言级别提供的 goroutine 间的通信方式,我们可以使用 channel 在多个 goroutine 之间传递消息。channel是进程内的通信方式,
声明和使用
声明channel可以使用 var 或者 make的方式。
关闭channel :close ()
发送和接收数据:
func main() {
ch := make(chan string)
go func() {
ch <- "abc"
}()
msg := <-ch
}
无缓冲 channel“
缓冲区大小则默认为 0。接受者会阻塞等待并阻塞应用程序,直至收到通信和接收到数据。
缓冲 channel
其缓存区大小是根据所设置的值来调整。在当缓冲区满了后,发送者就会阻塞并等待。而当缓冲区为空时,接受者就会阻塞并等待,直至有新的数据
// 无缓冲
ch1 := make(chan int)
// 缓冲区为 3
ch2 := make(chan int, 3)
channel底层数据结构
本质上 channel 在设计上就是环形队列。其包含发送方队列、接收方队列,加上互斥锁 mutex 等结构。
// src/runtime/chan.go
type hchan struct {
qcount uint 队列中的元素总数量
dataqsiz uint 循环队列长度
buf unsafe.Pointer 指向长度为dataqsiz的底层环形数组
elemsize uint16 能够接受和发送的元素大小。
closed uint32 是否关闭
elemtype *_type 能够接受和发送的元素类型
sendx uint 已发送元素在循环队列中的索引位置。
recvx uint 已接收元素在循环队列中的索引位置。
recvq waitq 接受者的sudog等待队列(阻塞的goroutine)(双向链表)
sendq waitq 发送者的sudog等待队列阻塞的goroutine)(双向链表)
lock mutex
}
channel 工作原理
-
创建channel
调用 mallocgc 从 堆内存 中分配一块连续的内存 -
发送数据
1 前置判断 当前 channel是否被阻塞,是否为空,对非阻塞进行快速失败检查
2 加互斥锁
3 判断 channel是否关闭
4 发送数据 : 带缓冲的 -->判断缓冲区剩余空间–>将数据拷贝到缓冲区–>sendx发送队列+1,队列长度+1 --> 解锁 -
接收数据
1 前置判断 当前 channel是否被阻塞,是否为空,对非阻塞进行快速失败检查
2 判断是否关闭 有无数据 :无数据 则返回失败
3 直接接收 :发现有正在阻塞的发送方,直接接收
4 缓冲接收 :缓冲区有数据时,根据recv索引位置取出数据,将数据拷贝到接收的数据结构中
4阻塞接收 :当发现 channel 上既没有待发送的 goroutine,缓冲区也没有数据时。将会进入到最后一个阶段阻塞接收,将自己 放入 sendq 阻塞队列 ,等待唤醒。 -
关闭channel
1 前置检查 :基本检查和关闭标志设置,保证 channel 不为 nil 和未关闭,保证边界
2 将 发送者 和接受者 的阻塞队列 中的 goruntine 放入 清除列表 glist ,释放为 可执行状态
向已关闭的Channel发送
向已关闭的Channel发送,会报panic
向已关闭的Channel关闭,会报panic
从已关闭的Channel读数据,先读完缓冲区内容,之后会读出来0(各数据类型的默认值)
golang context
Golang context 主要在异步场景中用于实现并发协调以及对 goroutine 的生命周期控制. 优雅的结束 goroutine
除此之外,context 还兼有一定的数据存储能力
context翻译成中文是”上下文”,即它可以控制一组呈树状结构的goroutine,每个goroutine拥有相同的上下文。
context 的 结构为一棵树
context 接口
context 为 interface接口类型 共有四个 api
type Context interface {
Deadline() (deadline time.Time, ok bool) 返回context过期时间
Done() <-chan struct{} 标记context是否结束的channle 空struct类型
Err() error 返回错误信息
Value(key any) any 用于传参context 返回 key值
}
常见 有两种 err
• Canceled:context 被 cancel 时会报此错误;
• DeadlineExceeded:context 超时时会报此错误.
emptyCtx:一个空的ctx,一般用于做根节点
emptyCtx 是一个空的 context,本质上类型为一个整型;
实现方法 :context.TODO() context.Background()
两者本质上没有区别
cancelCtx:核心,用来处理取消相关的操作。
type cancelCtx struct {
Context
mu sync.Mutex 互斥锁
done atomic.Value
children map[canceler]struct{} 儿子context map
err error 记录了当前 cancelCtx 的错误:必然为子context错误
}
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
实现方法:Withcancel()
这个函数相当重要,会根据传入的context生成一个子context和一个取消函数。当父context有相关取消操作,或者直接调用cancel函数的话,子context就会被取消。
过程:
前置判断,看是否为异常情况。
关闭c.done,这样外部调用cancelCtx.Done()就会有返回结果。
递归调用子节点的cancel方法。
视情况从父节点中移除子节点。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
• 校验父 context 非空;
• 注入父 context 构造好一个新的 cancelCtx;
• 在 propagateCancel方法内启动一个守护协程,以保证父context终止时,该 cancelCtx 也被终止;
• 将 cancelCtx 返回,连带返回一个用以终止该 cancelCtx 的闭包函数.
propagateCancel :用以传递父子 context 之间的 cancel 事件
timerCtx:用来处理超时相关操作。
timerCtx 在 cancelCtx 基础上又做了一层封装,
新增了一个 time.Timer 用于定时终止 context;
另外新增了一个 deadline 字段用于字段 timerCtx 的过期时间
type timerCtx struct {
cancelCtx
timer *time.Timer //用于在过期时间终止context
deadline time.Time //过期时间
}
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
return c.deadline, true //返回过期时间
}
对外实现
**WithTimeout() WithDeadlint() **
下面代码可看出
:WithTimeout 传入相对的时间段 time.Duration
WithDeadline 传入绝对时间点 time.time
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
valueCtx:用来传参,传递数据
数常用来传递数据,保存一些链路追踪信息(traceid),ip、请求参数等
valueCtx 不适合视为存储介质存放大量的 kv 数据
原因 :
一个 valueCtx 实例只能存一个 kv 对 ,
valueCtx 匹配过程可能要找父节点 ,会到O(n)
不支持基于 k 的去重
type valueCtx struct {
Context
key, val any
}
• valueCtx 同样继承了一个 parent context;
• 一个 valueCtx 中仅有一组 kv 对.
实现 WithValue ()
func (c *valueCtx) Value(key any) any {
if c.key == key {
return c.val
}
return value(c.Context, key)
}
假如当前 valueCtx 的 key 等于用户传入的 key,则直接返回其 value;
假如不等,则从 父 context 中依次向上寻找.
也就是说 valueCtx的作用域为 当前 context 和 子 context
sync.WaitGroup
WaitGroup是Golang应用开发过程中经常使用的并发控制技术。
WaitGroup,可理解为Wait-Goroutine-Group,即等待一组goroutine结束。
使用
WaitGroup对外提供三个接口:
Add(delta int):
将delta值加到counter中 , 当counter 为 0 则 根据 waiter数量 释放对应数量的 信号量
counter 小于 0 则panic
Done():
counter递减1
Wait():
wait做了两件事,一是累加waiter, 二是阻塞并等待信号量
数据结构
type WaitGroup struct {
state1 [3]uint32
}
state1是个长度为3的数组,其中包含了state和一个信号量,而state实际上是两个计数器:
counter: 当前还未执行结束的goroutine计数器
waiter count: 等待goroutine-group结束的goroutine数量,即有多少个等候者
semaphore: 信号量
GMP调度模型
GMP是Go运行时调度层面的实现,包含4个重要结构,分别是G、M、P、Sched
组成部分
G(go协程)
储了 Goroutine 的执行栈信息、Goroutine 状态以及 Goroutine 的任务函数等。G的数量无限制,理论上只受内存的影响,
创建一个 G 的初始栈大小为2-4K
M (线程)
Go 对操作系统线程(OS thread)的封装,可以看作操作系统内核线程
最大数量数:默认数量限制是 10000,可以通过 debug.SetMaxThreads() 方法进行设置
闲置会睡眠或被回收
P(虚拟处理器)
M执行G所需要的资源和上下文,只有将 P 和 M 绑定,才能让 P 中的 G 真正运行起来
P维护了一个本地队列(数组),用来存放G
runnext字段 :即将被执行的G
Sched(调度器结构)
维护了全局队列和以及调度器的一些状态信息
全局队列(链表Gqueue)
//src/runtime/runtime2.go
type g struct {
goid int64 // 唯一的goroutine的ID
sched gobuf // goroutine切换时,用于保存g的上下文
stack stack // 栈
gopc // pc of go statement that created this goroutine
startpc uintptr // pc of goroutine function
...
}
type p struct {
lock mutex
id int32
status uint32 // one of pidle/prunning/...
// Queue of runnable goroutines. Accessed without lock.
runqhead uint32 // 本地队列队头
runqtail uint32 // 本地队列队尾
runq [256]guintptr // 本地队列,大小256的数组,数组往往会被都读入到缓存中,对缓存友好,效率较高
runnext guintptr // 下一个优先执行的goroutine(一定是最后生产出来的),为了实现局部性原理,runnext中的G永远会被最先调度执行
...
}
type m struct {
g0 *g
// 每个M都有一个自己的G0,不指向任何可执行的函数,在调度或系统调用时,M会切换到G0,使用G0的栈空间来调度
curg *g
// 当前正在执行的G
...
}
type schedt struct {
...
runq gQueue // 全局队列,链表(长度无限制)
runqsize int32 // 全局队列长度
...
}
GM模型
Go早期是GM模型,没有P组件
缺点:
1.全局队列的锁竞争,当 M 从全局队列中添加或者获取 G 的时候,都需要获取队列锁,导致激烈的锁竞争
2.M 转移 G 增加额外开销,当 M1 在执行 G1 的时候, M1 创建了 G2,为了继续执行 G1,需要把 G2 保存到全局队列中,无法保证G2是被M1处理。因为 M1 原本就保存了 G2 的信息,所以 G2 最好是在 M1 上执行,这样的话也不需要转移G到全局队列和线程上下文切换
3.线程使用效率不能最大化,没有work-stealing 和hand-off 机制
调度过程
线程复用(work stealing 机制和hand off 机制)
利用并行(利用多核CPU)
抢占调度(解决公平性问题)
G的来源
P的runnext(只有1个G,局部性原理,永远会被最先调度执行)
P的本地队列(数组,最多256个G)
全局G队列(链表,无限制)
网络轮询器network poller(存放网络调用被阻塞的G)
P的来源
全局P队列(数组,GOMAXPROCS个P)
M的来源
休眠线程队列(未绑定P,长时间休眠会等待GC回收销毁)
运行线程(绑定P,指向P中的G)
自旋线程(绑定P,指向M的G0)
运行过程
步骤 1:创建 G,关键字 go func() 创建 G
步骤 2:保存 G,创建的 G 优先保存到本地队列 P,如果 P 满了,则会平衡部分P到全局队列中
每个协程 G 都会被尝试先放到 P 中的 runnext,若 runnext 为空则放到 runnext 中,生产结束
若 runnext 满,则将原来 runnext 中的 G 踢到本地队列中,将当前 G 放到 runnext 中,生产结束
若本地队列也满了,则将本地队列中的 G 拿出一半,放到全局队列中,生产结束。
步骤3:唤醒或者新建M执行任务,进入调度循环(步骤4,5,6)
步骤 4:M 获取 G,M首先从P的本地队列获取 G,如果 P为空,则从全局队列获取 G,如果全局队列也为空,则从另一个本地队列偷取一半数量的 G(负载均衡),这种从其它P偷的方式称之为 work stealing。
(从全局队列中获取G时,最后获取P自身长度的一半,方便后来新建的G直接加入本地队列)
步骤5:M 调度和执行 G,M调用 G.func() 函数执行 G
M在执行 G 的过程发生系统调用阻塞(同步),会阻塞G和M(操作系统限制),此时P会和当前M解绑,并寻找新的M,如果没有空闲的M就会新建一个M ,接管正在阻塞G所属的P,接着继续执行 P中其余的G,这种阻塞后释放P的方式称之为hand off。
系统调用结束后,这个G会尝试获取一个空闲的P执行,优先获取之前绑定的P,并放入到这个P的本地队列,如果获取不到P,那么这个线程M变成休眠状态,加入到空闲线程中,然后这个G会被放入到全局队列中。
如果M在执行G的过程发生网络IO等操作阻塞时(异步),阻塞G,不会阻塞M。M会寻找P中其它可执行的G继续执行,G会被网络轮询器network poller 接手,当阻塞的G恢复后,G1从network poller 被移回到P的 本地队列中
步骤6:M执行完G后清理现场,重新进入调度循环
调度时机
在以下情形下,会切换正在执行的goroutine
抢占式调度
sysmon 检测到协程运行过久(比如sleep,死循环)
切换到g0,进入调度循环
主动调度
新起一个协程和协程执行完毕
触发调度循环
主动调用runtime.Gosched()
切换到g0,进入调度循环
垃圾回收之后
stw之后,会重新选择g开始执行
被动调度
系统调用(比如文件IO)阻塞(同步)
阻塞G和M,P与M分离,将P交给其它M绑定,其它M执行P的剩余G
网络IO调用阻塞(异步)
阻塞G,G移动到NetPoller,M执行P的剩余G
atomic/mutex/channel等阻塞(异步)
阻塞G,G移动到channel的等待队列中,M执行P的剩余G
golang GC (垃圾回收)
GC版本变化
Go V1.3 标记清除(mark and sweep)
Go V1.5 三色标记法
Go V1.8 三色标记法+ 混合写屏障
GC的触发时机:
系统的定时触发:如果两分钟内没有触发GC,则会每隔两分钟进行触发一次GC。(gcTriggerTime)
用户显式调用:用户调用runtime.GC方法,进行强制触发
申请内存时触发:给对象申请堆空间的时候,可能会触发GC,调用mallocgc的方法 (gcTriggerHeap)
STW(stop the world)
stop-the-world,简称 STW,指的是 GC 事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为 STW。
三色标记算法
优点 :不需一次性扫描整个内存空间,可以减少stw的时间
原始标记清除算法带来的长时间STW, 为了解决这一问题,Go从V1.5版本实现了基于三色标记清除的并发垃圾收集算法
白色对象:引用不到 需要回收的对象
黑色对象:确定被使用到,不需要回收的对象
灰色对象:中间态,待处理队列
1.遍历根对象的第一层可达对象标记为灰色, 不可达默认白色。
2.将灰色对象的下一层可达对象标记为灰色, 自身标记为黑色。
3.多次重复步骤2, 直到灰色对象为0, 只剩下白色对象和黑色对象。
4.回收白色对象
混合写屏障 (结合 插入写屏障 、删除写屏障)
三色标记法的问题 :在gc中,程序也在运行,会产生新的对象,会被标记为白色,而被误杀gc掉,Gc过程中 修改对象或者删除对象也会影响gc的结果不准确。
混合写屏障 :监控对象的内存修改,并对对象进行重新标记. gc一旦开始,无论是创建对象还是对象的引用改变,都会先变为灰色。
插入写屏障:gc过程中,被引用的对象会被标记为灰色
删除写屏障 : Gc 过程中 被删除 的对象 被标记为灰色 (防止他影响后面的gc结果),也能预防野指针
golang 内存分配
1. Go内存分配设计原理
Go内存分配器的设计思想来源于TCMalloc,全称是Thread-Caching Malloc。核心思想:
多级管理和缓存:降低锁的粒度和开销
不同粒度的span:减少内存碎片
线程缓存(MCache):作为线程独立的内存池,与线程的第一交互内存,访问无需加锁;
中心缓存(MCentral):作为线程缓存的下一级,是多个线程共享的,所以访问时需要加锁;
页堆(MHeap):中心缓存的下一级,在遇到32KB以上的对象时,会直接选择页堆分配大内存
内存粒度划分(arena、span、page)
arena, span, page和内存块组成了堆内存。
一个arena 被划分为 多个span,一个span 中有多个page
一个arena对应一个heapArena结构,一个span对应一个mspan结构。
mspan
Go 包给出了67种预置的大小规格,最小8字节,最大32KB。按需把对象放到指定的span中,防止产生过多的内存碎片。
每个span 中包含多个
mspan管理着span中一组连续的page,划分的内存块规格类型记录在spanclass中
68种然后每种规格会按照是否不需要GC扫描进一步区分开来 ,包含指针和不包指针,所以共分为了 136种
nelems :记录着当前span共划分成了多少个内存块。
freeIndex :记录着下个空闲内存块的索引。
allocBits :位图用于标记哪些内存块已经被分配了。
heapAreana
heapArena这里存储着arena的元数据,里面有一群位图标记。
heapArena.bitmap :
bitmap位图:
(1)用一位标记这个arena中,一个指针大小的内存单元到底是指针还是标量;
(2)再用一位来标记这块内存空间的后续单元是否包含指针。
而且为了便于操作,bitmap中用一字节标记arena中4个指针大小的内存空间:低4位用于标记指针/标量;高4位用于标记扫描/终止。
pageInUse :是个uint8类型的数组,长度为1024,用来标记哪些页面被使用了。
**pageMarks:**只标记每个span的第一个page。在,标记哪些span中存在被标记的对象,GC会释放不包含这些span的空间
内存分配级别 (Mcache,Mcentral,Mheap)
Mheap
mheap管理了arean和一个全局的mspan
全局mspan 根据 span 的种类 分成了 大小135的数组 元素为:spanclass
由 mcentra 管理,一个mcentral对应一种mspan规格类型
mcentral
操作mcentral 需要加锁
实际上,一个mcentral对应一种mspan规格类型,同样记录在spanclass中,一共有136种。
mecentral 将span 分为 full(用尽) 和 partial (未用尽)
每一种又会放到两个并发安全的set中(spanset):一个是已清扫的;另一个是未清扫的
mcache
为降低多个P之间的竞争性,Go语言的每个P都有一个本地小对象缓存,也就是mcache,从这里取用就不用再加锁了。
mcache这里有一个长度为136的、*mspan类型的数组,还有专门用于分配小于16字节类型的tiny内存。
当前P需要用到特定规格类型的mspan时,先去本地缓存这里找对应的mspan;如果没有或者用完了,就去mcentral这里获取一个放到本地,把已用尽的归还到对应mcentral的full set中。
内存分配
golang 使用 mallocgc 函数来 分配内存 :工作流程如下
辅助GC: (对象标记,)
空间分配 (三种策略)
位图标记(标记内存使用位置等)
收尾工作(是否需要gc,减去内存对齐多算的内存等)
堆上所有的对象内存分配都会通过runtime.newobject进行分配,运行时根据对象大小将它们分为微对象、小对象和大对象:
微对象(0, 16B):使用微型分配器(tiny alloclator),分配在 mcache中的微对象区域(每个16字节),多个微对象 会被合并放在一个微对象区域中
小对象[16B, 32KB]:先向mcache申请,mcache内存空间不够时,向mcentral申请,mcentral不够,则向页堆mheap申请,再不够就向操作系统申请。
大对象(32KB, +∞):大对象直接向页堆mheap申请。
golang interface
interface类型定义了一组方法,如果某个对象实现了某个接口的所有方法,则此对象就实现了此接口。
interface值
如果我们定义了一个interface的变量,那么这个变量里面可以存实现这个interface的任意类型的对象
个人理解(有点像c++的模板)
interface函数参数
interface的变量可以持有任意实现该interface类型的对象
golang 与 面向对象
golang中的对象
尽管go中没有object这种类型,但是go中的struct有着跟object相同的特性。
golang中根据首字母的大小写来确定可以访问的权限。无论是方法名、常量、变量名还是结构体的名称,如果首字母大写,则可以被其他的包访问;如果首字母小写,则只能在本包中使用
golang 中的继承
方法继承
指子类获得父类的属性和方法, 在go中就是使用结构体嵌套结构体
多态
go 通过 interface实现多态
调用该匿名字段的 struct 可以重写 该匿名字段实现的方法
golang 内存泄漏 内存逃逸
内存泄漏如何定位和处理
可能发生内存泄漏的场景
- 申请过多的goroutine 例如在for循环中申请过多的goroutine来不及释放导致内存泄漏
- 协程结束时协程中的channel没有关闭,导致一直阻塞;例如协程中有死循环
- 切片截取引起子切片内存泄漏 解决 : append
排查内存泄漏 :pprof 分析工具
pprof 支持四种类型的分析:
CPU分析
Memory内存分析
Block阻塞分析,
Mutex互斥锁分析
golang 内存 逃逸
内存分为 堆 栈
堆内存(Heap) 一般来讲是人为手动进行管理,手动申请、分配、释放。一般硬件内存有多大堆内存就有多大。适合不可预知大小的内存分配,分配速度较慢,而且会形成内存碎片。
栈内存(Stack) 是一种拥有先进先出线性表数据结构。由编译器进行管理,自动申请、分配、释放。通过栈指针分配和访问,速度比较快,而堆内存则需要进行内存寻址
什么是内存逃逸
在Go语言中,编译器会尽可能地将变量分配在栈上,以提高程序的性能。但是,有些情况下变量会逃逸到堆上,这就是内存逃逸。
产生内存逃逸的原因
变量逃逸到堆上的主要原因是变量的生命周期超出了函数的作用域。
例如,将局部变量分配给全局变量或返回指向局部变量的指针,这样变量就会逃逸到堆上。
变量逃逸到堆上的另一个原因是变量被多个协程(Goroutine)共享。当多个协程同时引用同一个变量时,为了保证变量的可见性和正确性,变量需要分配在堆上。
变量的大小超过了栈的限制。栈上的内存空间通常较小,如果变量的大小超过了栈的限制,编译器会将变量分配到堆上。
以下是一些导致内存逃逸的情况:
在函数内部创建的变量被返回或存储到全局变量中。
在函数内部创建的变量被存储到外部数据结构中,如切片或映射。
在函数内部创建的变量被传递给其他函数,并在其他函数中被引用。
go 查看内存逃逸 :
Go 语言工具链 go build 时,配合使用参数 -gcflags
go build -gcflags '-m -l' main.go
我们还可以使用一种更底层的,更硬核,也更准确的方式来判断一个对象是否逃逸,那就是: 通过反编译命令查看
go tool compile -S main.go
内存逃逸分析的意义
通过逃逸分析,可以尽量把那些不需要分配到堆上的变量直接分配到栈上,堆上的变量少了,会减轻分配堆内存的开销,同时也会减少GC的压力,提高程序的运行速度。
怎么避免内存逃逸
尽量减少外部指针引用,必要的时候可以使用值传递;
对于自己定义的数据大小,有一个基本的预判,尽量不要出现栈空间溢出的情况;
Golang中的接口类型的方法调用是动态调度,如果对于性能要求比较高且访问频次比较高的函数调用,应该尽量避免使用接口类型;
尽量不要写闭包函数,可读性差且发生逃逸。
Golang 并发控制demo
并发执行在Golang中很容易实现,只需要go func(),大多数我们会把一个大的任务拆分成多个子任务去执行,这时候我们就需要关心子任务是否执行成功和结束,需要收到信息进行下一步程序的执行。在golang中,我整理了三种Goroutine常用的控制方式。
1:Sync.WaitGroup:
package main
import (
"context"
"fmt"
"net/http"
"sync"
"time"
)
```go
func main() {
var wg sync.WaitGroup
//2个task
wg.Add(2)
go func() {
defer wg.Done()
fmt.Println("goroutine 1 done")
}()
go func() {
defer wg.Done()
fmt.Println("goroutine 2 done")
}()
wg.Wait()
fmt.Println("all goroutine done")
}
2:Channel
使用场景:当一个主任务拆分为子任务去执行,子任务全部执行完毕,通过channel来通知主任务执行完毕,主任务继续向下执行。比较适用于层级比较少的主任务和子任务间的通信。
package main
import (
"context"
"fmt"
"net/http"
"sync"
"time"
)
func main() {
ch := make(chan int)
flag := make(chan bool)
go son(ch, flag)
for i := 0; i < 10; i++ {
ch <- i
}
flag <- true
fmt.Println("all is over")
}
func son(ch chan int, flag chan bool) {
t := time.Tick(time.Second)
for _ = range t {
select {
case msg := <-ch:
fmt.Println("print", msg)
case <-flag:
fmt.Println("goroutine is over")
}
}
}
3:Context
package main
import (
"context"
"fmt"
"net/http"
"sync"
"time"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
go foo(ctx, "Sonbar")
fmt.Println("subwork is starting")
time.Sleep(5 * time.Second)
//fiveminutes is over
cancel()
//allgoroutine over
time.Sleep(3 * time.Second)
fmt.Println("main work over")
}
func foo(ctx context.Context, name string) {
go bar(ctx, name)
for {
select {
case <-ctx.Done():
fmt.Println(name, "goroutine A Exit")
return
case <-time.After(1 * time.Second):
fmt.Println(name, "goroutine A do something")
}
}
}
func bar(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Println(name, "goroutine B Exit")
return
case <-time.After(2 * time.Second):
fmt.Println(name, "goroutine B do something")
}
}
}
使用场景:适用于多层Goroutine嵌套和组合,当然Context包不仅仅用于并发控制,还有更多的功能和场景需要我们去探索。
golang 异步demo
ch := make(chan int)
go func (){
time.Sleep(1 * time.Second)
ch <- 1
}()
result := <- ch
fmt.Println(result)